var Insertr = (function () { 'use strict'; /** * InsertrCore - Core functionality for content management */ class InsertrCore { constructor(options = {}) { this.options = { apiEndpoint: options.apiEndpoint || '/api/content', siteId: options.siteId || 'default', ...options }; } // Find all enhanced elements on the page with container expansion findEnhancedElements() { const directElements = document.querySelectorAll('.insertr'); const expandedElements = []; directElements.forEach(element => { if (this.isContainer(element) && !element.classList.contains('insertr-group')) { // Container element (.insertr) - expand to viable children const children = this.findViableChildren(element); expandedElements.push(...children); } else { // Regular element or group (.insertr-group) expandedElements.push(element); } }); return expandedElements; } // Check if element is a container that should expand to children isContainer(element) { const containerTags = new Set([ 'div', 'section', 'article', 'header', 'footer', 'main', 'aside', 'nav' ]); return containerTags.has(element.tagName.toLowerCase()); } // Find viable children for editing (elements with only text content) findViableChildren(containerElement) { const viable = []; for (const child of containerElement.children) { // Skip elements that already have .insertr class if (child.classList.contains('insertr')) { continue; } // Skip self-closing elements if (this.isSelfClosing(child)) { continue; } // Check if element has only text content (no nested HTML elements) if (this.hasOnlyTextContent(child)) { viable.push(child); } } return viable; } // Check if element is viable for editing (allows simple formatting) hasOnlyTextContent(element) { // Allow elements with simple formatting tags const allowedTags = new Set(['strong', 'b', 'em', 'i', 'a', 'span', 'code']); for (const child of element.children) { const tagName = child.tagName.toLowerCase(); // If child is not an allowed formatting tag, reject if (!allowedTags.has(tagName)) { return false; } // If formatting tag has nested complex elements, reject if (child.children.length > 0) { // Recursively check nested content isn't too complex for (const nestedChild of child.children) { const nestedTag = nestedChild.tagName.toLowerCase(); if (!allowedTags.has(nestedTag)) { return false; } } } } // Element has only text and/or simple formatting - this is viable return element.textContent.trim().length > 0; } // Check if element is self-closing isSelfClosing(element) { const selfClosingTags = new Set([ 'img', 'input', 'br', 'hr', 'meta', 'link', 'area', 'base', 'col', 'embed', 'source', 'track', 'wbr' ]); return selfClosingTags.has(element.tagName.toLowerCase()); } // Get element metadata getElementMetadata(element) { const existingId = element.getAttribute('data-content-id'); // Ensure element has insertr class for server processing if (!element.classList.contains('insertr')) { element.classList.add('insertr'); } // Send HTML markup to server for unified ID generation return { contentId: existingId, // null if new content, existing ID if updating contentType: element.getAttribute('data-content-type') || this.detectContentType(element), element: element, htmlMarkup: element.outerHTML // Server will generate ID from this }; } // Get current file path from URL for consistent ID generation getCurrentFilePath() { const path = window.location.pathname; if (path === '/' || path === '') { return 'index.html'; } // Remove leading slash: "/about.html" → "about.html" return path.replace(/^\//, ''); } // Detect content type for elements without data-content-type detectContentType(element) { const tag = element.tagName.toLowerCase(); if (element.classList.contains('insertr-group')) { return 'markdown'; } switch (tag) { case 'h1': case 'h2': case 'h3': case 'h4': case 'h5': case 'h6': return 'text'; case 'p': return 'textarea'; case 'a': case 'button': return 'link'; case 'div': case 'section': return 'markdown'; case 'span': return 'markdown'; // Match backend: spans support inline markdown default: return 'text'; } } // Get all elements with their metadata, including group elements getAllElements() { const directElements = document.querySelectorAll('.insertr, .insertr-group'); const processedElements = []; directElements.forEach(element => { if (element.classList.contains('insertr-group')) { // Group element - treat as single editable unit processedElements.push(element); } else if (this.isContainer(element)) { // Container element - expand to children const children = this.findViableChildren(element); processedElements.push(...children); } else { // Regular element processedElements.push(element); } }); return Array.from(processedElements).map(el => this.getElementMetadata(el)); } } /** * marked v16.2.1 - 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 L(){return {async:!1,breaks:!1,extensions:null,gfm:!0,hooks:null,pedantic:!1,renderer:null,silent:!1,tokenizer:null,walkTokens:null}}var O=L();function H(l){O=l;}var E={exec:()=>null};function h(l,e=""){let t=typeof l=="string"?l:l.source,n={replace:(r,i)=>{let s=typeof i=="string"?i:i.source;return s=s.replace(m.caret,"$1"),t=t.replace(r,s),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")},xe=/^(?:[ \t]*(?:\n|$))+/,be=/^((?: {4}| {0,3}\t)[^\n]+(?:\n(?:[ \t]*(?:\n|$))*)?)+/,Re=/^ {0,3}(`{3,}(?=[^`\n]*(?:\n|$))|~{3,})([^\n]*)(?:\n|$)(?:|([\s\S]*?)(?:\n|$))(?: {0,3}\1[~`]* *(?=\n|$)|$)/,C=/^ {0,3}((?:-[\t ]*){3,}|(?:_[ \t]*){3,}|(?:\*[ \t]*){3,})(?:\n+|$)/,Oe=/^ {0,3}(#{1,6})(?=\s|$)(.*)(?:\n+|$)/,j=/(?:[*+-]|\d{1,9}[.)])/,se=/^(?!bull |blockCode|fences|blockquote|heading|html|table)((?:.|\n(?!\s*?\n|bull |blockCode|fences|blockquote|heading|html|table))+?)\n {0,3}(=+|-+) *(?:\n+|$)/,ie=h(se).replace(/bull/g,j).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(),Te=h(se).replace(/bull/g,j).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(),F=/^([^\n]+(?:\n(?!hr|heading|lheading|blockquote|fences|list|html|table| +\n)[^\n]+)*)/,we=/^[^\n]+/,Q=/(?!\s*\])(?:\\[\s\S]|[^\[\]\\])+/,ye=h(/^ {0,3}\[(label)\]: *(?:\n[ \t]*)?([^<\s][^\s]*|<.*?>)(?:(?: +(?:\n[ \t]*)?| *\n[ \t]*)(title))? *(?:\n+|$)/).replace("label",Q).replace("title",/(?:"(?:\\"?|[^"\\])*"|'[^'\n]*(?:\n[^'\n]+)*\n?'|\([^()]*\))/).getRegex(),Pe=h(/^( {0,3}bull)([ \t][^\n]+?)?(?:\n|$)/).replace(/bull/g,j).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",U=/|$))/,Se=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",U).replace("tag",v).replace("attribute",/ +[a-zA-Z:_][\w.:-]*(?: *= *"[^"\n]*"| *= *'[^'\n]*'| *= *[^\s"'=<>`]+)?/).getRegex(),oe=h(F).replace("hr",C).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(),$e=h(/^( {0,3}> ?(paragraph|[^\n]*)(?:\n|$))+/).replace("paragraph",oe).getRegex(),K={blockquote:$e,code:be,def:ye,fences:Re,heading:Oe,hr:C,html:Se,lheading:ie,list:Pe,newline:xe,paragraph:oe,table:E,text:we},re=h("^ *([^\\n ].*)\\n {0,3}((?:\\| *)?:?-+:? *(?:\\| *:?-+:? *)*(?:\\| *)?)(?:\\n((?:(?! *\\n|hr|heading|blockquote|code|fences|list|html).*(?:\\n|$))*)\\n*|$)").replace("hr",C).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(),_e={...K,lheading:Te,table:re,paragraph:h(F).replace("hr",C).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()},Le={...K,html:h(`^ *(?:comment *(?:\\n|\\s*$)|<(tag)[\\s\\S]+? *(?:\\n{2,}|\\s*$)|\\s]*)*?/?> *(?:\\n{2,}|\\s*$))`).replace("comment",U).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:E,lheading:/^(.+?)\n {0,3}(=+|-+) *(?:\n+|$)/,paragraph:h(F).replace("hr",C).replace("heading",` *#{1,6} *[^ ]`).replace("lheading",ie).replace("|table","").replace("blockquote"," {0,3}>").replace("|fences","").replace("|list","").replace("|html","").replace("|tag","").getRegex()},Me=/^\\([!"#$%&'()*+,\-./:;<=>?@\[\]\\^_`{|}~])/,ze=/^(`+)([^`]|[^`][\s\S]*?[^`])\1(?!`)/,ae=/^( {2,}|\\)\n(?!\s*$)/,Ae=/^(`+|[^`])(?:(?= {2,}\n)|[\s\S]*?(?:(?=[\\]*?>/g,pe=/^(?:\*+(?:((?!\*)punct)|[^\s*]))|^_+(?:((?!_)punct)|([^\s_]))/,qe=h(pe,"u").replace(/punct/g,D).getRegex(),ve=h(pe,"u").replace(/punct/g,ue).getRegex(),ce="^[^_*]*?__[^_*]*?\\*[^_*]*?(?=__)|[^*]+(?=[^*])|(?!\\*)punct(\\*+)(?=[\\s]|$)|notPunctSpace(\\*+)(?!\\*)(?=punctSpace|$)|(?!\\*)punctSpace(\\*+)(?=notPunctSpace)|[\\s](\\*+)(?!\\*)(?=punct)|(?!\\*)punct(\\*+)(?!\\*)(?=punct)|notPunctSpace(\\*+)(?=notPunctSpace)",De=h(ce,"gu").replace(/notPunctSpace/g,le).replace(/punctSpace/g,W).replace(/punct/g,D).getRegex(),Ze=h(ce,"gu").replace(/notPunctSpace/g,Ie).replace(/punctSpace/g,Ce).replace(/punct/g,ue).getRegex(),Ge=h("^[^_*]*?\\*\\*[^_*]*?_[^_*]*?(?=\\*\\*)|[^_]+(?=[^_])|(?!_)punct(_+)(?=[\\s]|$)|notPunctSpace(_+)(?!_)(?=punctSpace|$)|(?!_)punctSpace(_+)(?=notPunctSpace)|[\\s](_+)(?!_)(?=punct)|(?!_)punct(_+)(?!_)(?=punct)","gu").replace(/notPunctSpace/g,le).replace(/punctSpace/g,W).replace(/punct/g,D).getRegex(),He=h(/\\(punct)/,"gu").replace(/punct/g,D).getRegex(),Ne=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(U).replace("(?:-->|$)","-->").getRegex(),Fe=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=/(?:\[(?:\\[\s\S]|[^\[\]\\])*\]|\\[\s\S]|`[^`]*`|[^\[\]\\`])*?/,Qe=h(/^!?\[(label)\]\(\s*(href)(?:(?:[ \t]*(?:\n[ \t]*)?)(title))?\s*\)/).replace("label",q).replace("href",/<(?:\\.|[^\n<>\\])+>|[^ \t\n\x00-\x1f]*/).replace("title",/"(?:\\"?|[^"\\])*"|'(?:\\'?|[^'\\])*'|\((?:\\\)?|[^)\\])*\)/).getRegex(),he=h(/^!?\[(label)\]\[(ref)\]/).replace("label",q).replace("ref",Q).getRegex(),de=h(/^!?\[(ref)\](?:\[\])?/).replace("ref",Q).getRegex(),Ue=h("reflink|nolink(?!\\()","g").replace("reflink",he).replace("nolink",de).getRegex(),X={_backpedal:E,anyPunctuation:He,autolink:Ne,blockSkip:Be,br:ae,code:ze,del:E,emStrongLDelim:qe,emStrongRDelimAst:De,emStrongRDelimUnd:Ge,escape:Me,link:Qe,nolink:de,punctuation:Ee,reflink:he,reflinkSearch:Ue,tag:Fe,text:Ae,url:E},Ke={...X,link:h(/^!?\[(label)\]\((.*?)\)/).replace("label",q).getRegex(),reflink:h(/^!?\[(label)\]\s*\[([^\]]*)\]/).replace("label",q).getRegex()},N={...X,emStrongRDelimAst:Ze,emStrongLDelim:ve,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\S]|[^\\])*?(?:\\[\s\S]|[^\s~\\]))\1(?=[^~]|$)/,text:/^([`~]+|[^`~])(?:(?= {2,}\n)|(?=[a-zA-Z0-9.!#$%&'*+\/=?_`{\|}~-]+@)|[\s\S]*?(?:(?=[\\":">",'"':""","'":"'"},ke=l=>Xe[l];function w(l,e){if(e){if(m.escapeTest.test(l))return l.replace(m.escapeReplace,ke)}else if(m.escapeTestNoEncode.test(l))return l.replace(m.escapeReplaceNoEncode,ke);return l}function J(l){try{l=encodeURI(l).replace(m.percentDecode,"%");}catch{return null}return l}function V(l,e){let t=l.replace(m.findPipe,(i,s,o)=>{let a=!1,u=s;for(;--u>=0&&o[u]==="\\";)a=!a;return a?"|":" |"}),n=t.split(m.splitPipe),r=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 fe(l,e,t,n,r){let i=e.href,s=e.title||null,o=l[1].replace(r.other.outputLinkReplace,"$1");n.state.inLink=!0;let a={type:l[0].charAt(0)==="!"?"image":"link",raw:t,href:i,title:s,text:o,tokens:n.inlineTokens(o)};return n.state.inLink=!1,a}function Je(l,e,t){let n=l.match(t.other.indentCodeCompensation);if(n===null)return e;let r=n[1];return e.split(` `).map(i=>{let s=i.match(t.other.beginningSpace);if(s===null)return i;let[o]=s;return o.length>=r.length?i.slice(r.length):i}).join(` `)}var y=class{options;rules;lexer;constructor(e){this.options=e||O;}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:z(n,` `)}}}fences(e){let t=this.rules.block.fences.exec(e);if(t){let n=t[0],r=Je(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:r}}}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 r=z(n,"#");(this.options.pedantic||!r||this.rules.other.endingSpaceChar.test(r))&&(n=r.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:z(t[0],` `)}}blockquote(e){let t=this.rules.block.blockquote.exec(e);if(t){let n=z(t[0],` `).split(` `),r="",i="",s=[];for(;n.length>0;){let o=!1,a=[],u;for(u=0;u1,i={type:"list",raw:"",ordered:r,start:r?+n.slice(0,-1):"",loose:!1,items:[]};n=r?`\\d{1,9}\\${n.slice(-1)}`:`\\${n}`,this.options.pedantic&&(n=r?n:"[*+-]");let s=this.rules.other.listItemRegex(n),o=!1;for(;e;){let u=!1,p="",c="";if(!(t=s.exec(e))||this.rules.block.hr.test(e))break;p=t[0],e=e.substring(p.length);let f=t[2].split(` `,1)[0].replace(this.rules.other.listReplaceTabs,Z=>" ".repeat(3*Z.length)),k=e.split(` `,1)[0],x=!f.trim(),g=0;if(this.options.pedantic?(g=2,c=f.trimStart()):x?g=t[1].length+1:(g=t[2].search(this.rules.other.nonSpaceChar),g=g>4?1:g,c=f.slice(g),g+=t[1].length),x&&this.rules.other.blankLine.test(k)&&(p+=k+` `,e=e.substring(k.length+1),u=!0),!u){let Z=this.rules.other.nextBulletRegex(g),ee=this.rules.other.hrRegex(g),te=this.rules.other.fencesBeginRegex(g),ne=this.rules.other.headingBeginRegex(g),me=this.rules.other.htmlBeginRegex(g);for(;e;){let G=e.split(` `,1)[0],A;if(k=G,this.options.pedantic?(k=k.replace(this.rules.other.listReplaceNesting," "),A=k):A=k.replace(this.rules.other.tabCharGlobal," "),te.test(k)||ne.test(k)||me.test(k)||Z.test(k)||ee.test(k))break;if(A.search(this.rules.other.nonSpaceChar)>=g||!k.trim())c+=` `+A.slice(g);else {if(x||f.replace(this.rules.other.tabCharGlobal," ").search(this.rules.other.nonSpaceChar)>=4||te.test(f)||ne.test(f)||ee.test(f))break;c+=` `+k;}!x&&!k.trim()&&(x=!0),p+=G+` `,e=e.substring(G.length+1),f=A.slice(g);}}i.loose||(o?i.loose=!0:this.rules.other.doubleBlankLine.test(p)&&(o=!0));let T=null,Y;this.options.gfm&&(T=this.rules.other.listIsTask.exec(c),T&&(Y=T[0]!=="[ ] ",c=c.replace(this.rules.other.listReplaceTask,""))),i.items.push({type:"list_item",raw:p,task:!!T,checked:Y,loose:!1,text:c,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 u=0;uf.type==="space"),c=p.length>0&&p.some(f=>this.rules.other.anyLine.test(f.raw));i.loose=c;}if(i.loose)for(let u=0;u({text:a,tokens:this.lexer.inline(a),header:!1,align:s.align[u]})));return s}}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 s=z(n.slice(0,-1),"\\");if((n.length-s.length)%2===0)return}else {let s=ge(t[2],"()");if(s===-2)return;if(s>-1){let a=(t[0].indexOf("!")===0?5:4)+t[1].length+s;t[2]=t[2].substring(0,s),t[0]=t[0].substring(0,a).trim(),t[3]="";}}let r=t[2],i="";if(this.options.pedantic){let s=this.rules.other.pedanticHrefTitle.exec(r);s&&(r=s[1],i=s[3]);}else i=t[3]?t[3].slice(1,-1):"";return r=r.trim(),this.rules.other.startAngleBracket.test(r)&&(this.options.pedantic&&!this.rules.other.endAngleBracket.test(n)?r=r.slice(1):r=r.slice(1,-1)),fe(t,{href:r&&r.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 r=(n[2]||n[1]).replace(this.rules.other.multipleSpaceGlobal," "),i=t[r.toLowerCase()];if(!i){let s=n[0].charAt(0);return {type:"text",raw:s,text:s}}return fe(n,i,n[0],this.lexer,this.rules)}}emStrong(e,t,n=""){let r=this.rules.inline.emStrongLDelim.exec(e);if(!r||r[3]&&n.match(this.rules.other.unicodeAlphaNumeric))return;if(!(r[1]||r[2]||"")||!n||this.rules.inline.punctuation.exec(n)){let s=[...r[0]].length-1,o,a,u=s,p=0,c=r[0][0]==="*"?this.rules.inline.emStrongRDelimAst:this.rules.inline.emStrongRDelimUnd;for(c.lastIndex=0,t=t.slice(-1*e.length+s);(r=c.exec(t))!=null;){if(o=r[1]||r[2]||r[3]||r[4]||r[5]||r[6],!o)continue;if(a=[...o].length,r[3]||r[4]){u+=a;continue}else if((r[5]||r[6])&&s%3&&!((s+a)%3)){p+=a;continue}if(u-=a,u>0)continue;a=Math.min(a,a+u+p);let f=[...r[0]][0].length,k=e.slice(0,s+r.index+f+a);if(Math.min(s,a)%2){let g=k.slice(1,-1);return {type:"em",raw:k,text:g,tokens:this.lexer.inlineTokens(g)}}let x=k.slice(2,-2);return {type:"strong",raw:k,text:x,tokens:this.lexer.inlineTokens(x)}}}}codespan(e){let t=this.rules.inline.code.exec(e);if(t){let n=t[2].replace(this.rules.other.newLineCharGlobal," "),r=this.rules.other.nonSpaceChar.test(n),i=this.rules.other.startingSpaceChar.test(n)&&this.rules.other.endingSpaceChar.test(n);return r&&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,r;return t[2]==="@"?(n=t[1],r="mailto:"+n):(n=t[1],r=n),{type:"link",raw:t[0],text:n,href:r,tokens:[{type:"text",raw:n,text:n}]}}}url(e){let t;if(t=this.rules.inline.url.exec(e)){let n,r;if(t[2]==="@")n=t[0],r="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."?r="http://"+t[0]:r=t[0];}return {type:"link",raw:t[0],text:n,href:r,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 b=class l{tokens;options;state;tokenizer;inlineQueue;constructor(e){this.tokens=[],this.tokens.links=Object.create(null),this.options=e||O,this.options.tokenizer=this.options.tokenizer||new y,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:I.normal,inline:M.normal};this.options.pedantic?(t.block=I.pedantic,t.inline=M.pedantic):this.options.gfm&&(t.block=I.gfm,this.options.breaks?t.inline=M.breaks:t.inline=M.gfm),this.tokenizer.rules=t;}static get rules(){return {block:I,inline:M}}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(r=s.call({lexer:this},e,t))?(e=e.substring(r.raw.length),t.push(r),!0):!1))continue;if(r=this.tokenizer.space(e)){e=e.substring(r.raw.length);let s=t.at(-1);r.raw.length===1&&s!==void 0?s.raw+=` `:t.push(r);continue}if(r=this.tokenizer.code(e)){e=e.substring(r.raw.length);let s=t.at(-1);s?.type==="paragraph"||s?.type==="text"?(s.raw+=(s.raw.endsWith(` `)?"":` `)+r.raw,s.text+=` `+r.text,this.inlineQueue.at(-1).src=s.text):t.push(r);continue}if(r=this.tokenizer.fences(e)){e=e.substring(r.raw.length),t.push(r);continue}if(r=this.tokenizer.heading(e)){e=e.substring(r.raw.length),t.push(r);continue}if(r=this.tokenizer.hr(e)){e=e.substring(r.raw.length),t.push(r);continue}if(r=this.tokenizer.blockquote(e)){e=e.substring(r.raw.length),t.push(r);continue}if(r=this.tokenizer.list(e)){e=e.substring(r.raw.length),t.push(r);continue}if(r=this.tokenizer.html(e)){e=e.substring(r.raw.length),t.push(r);continue}if(r=this.tokenizer.def(e)){e=e.substring(r.raw.length);let s=t.at(-1);s?.type==="paragraph"||s?.type==="text"?(s.raw+=(s.raw.endsWith(` `)?"":` `)+r.raw,s.text+=` `+r.raw,this.inlineQueue.at(-1).src=s.text):this.tokens.links[r.tag]||(this.tokens.links[r.tag]={href:r.href,title:r.title},t.push(r));continue}if(r=this.tokenizer.table(e)){e=e.substring(r.raw.length),t.push(r);continue}if(r=this.tokenizer.lheading(e)){e=e.substring(r.raw.length),t.push(r);continue}let i=e;if(this.options.extensions?.startBlock){let s=1/0,o=e.slice(1),a;this.options.extensions.startBlock.forEach(u=>{a=u.call({lexer:this},o),typeof a=="number"&&a>=0&&(s=Math.min(s,a));}),s<1/0&&s>=0&&(i=e.substring(0,s+1));}if(this.state.top&&(r=this.tokenizer.paragraph(i))){let s=t.at(-1);n&&s?.type==="paragraph"?(s.raw+=(s.raw.endsWith(` `)?"":` `)+r.raw,s.text+=` `+r.text,this.inlineQueue.pop(),this.inlineQueue.at(-1).src=s.text):t.push(r),n=i.length!==e.length,e=e.substring(r.raw.length);continue}if(r=this.tokenizer.text(e)){e=e.substring(r.raw.length);let s=t.at(-1);s?.type==="text"?(s.raw+=(s.raw.endsWith(` `)?"":` `)+r.raw,s.text+=` `+r.text,this.inlineQueue.pop(),this.inlineQueue.at(-1).src=s.text):t.push(r);continue}if(e){let s="Infinite loop on byte: "+e.charCodeAt(0);if(this.options.silent){console.error(s);break}else throw new Error(s)}}return this.state.top=!0,t}inline(e,t=[]){return this.inlineQueue.push({src:e,tokens:t}),t}inlineTokens(e,t=[]){let n=e,r=null;if(this.tokens.links){let o=Object.keys(this.tokens.links);if(o.length>0)for(;(r=this.tokenizer.rules.inline.reflinkSearch.exec(n))!=null;)o.includes(r[0].slice(r[0].lastIndexOf("[")+1,-1))&&(n=n.slice(0,r.index)+"["+"a".repeat(r[0].length-2)+"]"+n.slice(this.tokenizer.rules.inline.reflinkSearch.lastIndex));}for(;(r=this.tokenizer.rules.inline.anyPunctuation.exec(n))!=null;)n=n.slice(0,r.index)+"++"+n.slice(this.tokenizer.rules.inline.anyPunctuation.lastIndex);for(;(r=this.tokenizer.rules.inline.blockSkip.exec(n))!=null;)n=n.slice(0,r.index)+"["+"a".repeat(r[0].length-2)+"]"+n.slice(this.tokenizer.rules.inline.blockSkip.lastIndex);let i=!1,s="";for(;e;){i||(s=""),i=!1;let o;if(this.options.extensions?.inline?.some(u=>(o=u.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 u=t.at(-1);o.type==="text"&&u?.type==="text"?(u.raw+=o.raw,u.text+=o.text):t.push(o);continue}if(o=this.tokenizer.emStrong(e,n,s)){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 u=1/0,p=e.slice(1),c;this.options.extensions.startInline.forEach(f=>{c=f.call({lexer:this},p),typeof c=="number"&&c>=0&&(u=Math.min(u,c));}),u<1/0&&u>=0&&(a=e.substring(0,u+1));}if(o=this.tokenizer.inlineText(a)){e=e.substring(o.raw.length),o.raw.slice(-1)!=="_"&&(s=o.raw.slice(-1)),i=!0;let u=t.at(-1);u?.type==="text"?(u.raw+=o.raw,u.text+=o.text):t.push(o);continue}if(e){let u="Infinite loop on byte: "+e.charCodeAt(0);if(this.options.silent){console.error(u);break}else throw new Error(u)}}return t}};var P=class{options;parser;constructor(e){this.options=e||O;}space(e){return ""}code({text:e,lang:t,escaped:n}){let r=(t||"").match(m.notSpaceStart)?.[0],i=e.replace(m.endingNewline,"")+` `;return r?'
'+(n?i:w(i,!0))+`
`:"
"+(n?i:w(i,!0))+`
`}blockquote({tokens:e}){return `
${this.parser.parse(e)}
`}html({text:e}){return e}def(e){return ""}heading({tokens:e,depth:t}){return `${this.parser.parseInline(e)} `}hr(e){return `
`}list(e){let t=e.ordered,n=e.start,r="";for(let o=0;o `+r+" `}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+" "+w(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${r}`),` `+t+` `+r+`
    `}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 `${w(e,!0)}`}br(e){return "
    "}del({tokens:e}){return `${this.parser.parseInline(e)}`}link({href:e,title:t,tokens:n}){let r=this.parser.parseInline(n),i=J(e);if(i===null)return r;e=i;let s='
    ",s}image({href:e,title:t,text:n,tokens:r}){r&&(n=this.parser.parseInline(r,this.parser.textRenderer));let i=J(e);if(i===null)return w(n);e=i;let s=`${n}{let o=i[s].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 r={...n};if(r.async=this.defaults.async||r.async||!1,n.extensions&&(n.extensions.forEach(i=>{if(!i.name)throw new Error("extension name required");if("renderer"in i){let s=t.renderers[i.name];s?t.renderers[i.name]=function(...o){let a=i.renderer.apply(this,o);return a===!1&&(a=s.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 s=t[i.level];s?s.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);}),r.extensions=t),n.renderer){let i=this.defaults.renderer||new P(this.defaults);for(let s in n.renderer){if(!(s in i))throw new Error(`renderer '${s}' does not exist`);if(["options","parser"].includes(s))continue;let o=s,a=n.renderer[o],u=i[o];i[o]=(...p)=>{let c=a.apply(i,p);return c===!1&&(c=u.apply(i,p)),c||""};}r.renderer=i;}if(n.tokenizer){let i=this.defaults.tokenizer||new y(this.defaults);for(let s in n.tokenizer){if(!(s in i))throw new Error(`tokenizer '${s}' does not exist`);if(["options","rules","lexer"].includes(s))continue;let o=s,a=n.tokenizer[o],u=i[o];i[o]=(...p)=>{let c=a.apply(i,p);return c===!1&&(c=u.apply(i,p)),c};}r.tokenizer=i;}if(n.hooks){let i=this.defaults.hooks||new $;for(let s in n.hooks){if(!(s in i))throw new Error(`hook '${s}' does not exist`);if(["options","block"].includes(s))continue;let o=s,a=n.hooks[o],u=i[o];$.passThroughHooks.has(s)?i[o]=p=>{if(this.defaults.async)return Promise.resolve(a.call(i,p)).then(f=>u.call(i,f));let c=a.call(i,p);return u.call(i,c)}:i[o]=(...p)=>{let c=a.apply(i,p);return c===!1&&(c=u.apply(i,p)),c};}r.hooks=i;}if(n.walkTokens){let i=this.defaults.walkTokens,s=n.walkTokens;r.walkTokens=function(o){let a=[];return a.push(s.call(this,o)),i&&(a=a.concat(i.call(this,o))),a};}this.defaults={...this.defaults,...r};}),this}setOptions(e){return this.defaults={...this.defaults,...e},this}lexer(e,t){return b.lex(e,t??this.defaults)}parser(e,t){return R.parse(e,t??this.defaults)}parseMarkdown(e){return (n,r)=>{let i={...r},s={...this.defaults,...i},o=this.onError(!!s.silent,!!s.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"));s.hooks&&(s.hooks.options=s,s.hooks.block=e);let a=s.hooks?s.hooks.provideLexer():e?b.lex:b.lexInline,u=s.hooks?s.hooks.provideParser():e?R.parse:R.parseInline;if(s.async)return Promise.resolve(s.hooks?s.hooks.preprocess(n):n).then(p=>a(p,s)).then(p=>s.hooks?s.hooks.processAllTokens(p):p).then(p=>s.walkTokens?Promise.all(this.walkTokens(p,s.walkTokens)).then(()=>p):p).then(p=>u(p,s)).then(p=>s.hooks?s.hooks.postprocess(p):p).catch(o);try{s.hooks&&(n=s.hooks.preprocess(n));let p=a(n,s);s.hooks&&(p=s.hooks.processAllTokens(p)),s.walkTokens&&this.walkTokens(p,s.walkTokens);let c=u(p,s);return s.hooks&&(c=s.hooks.postprocess(c)),c}catch(p){return o(p)}}}onError(e,t){return n=>{if(n.message+=` Please report this to https://github.com/markedjs/marked.`,e){let r="

    An error occurred:

    "+w(n.message+"",!0)+"
    ";return t?Promise.resolve(r):r}if(t)return Promise.reject(n);throw n}}};var _=new B;function d(l,e){return _.parse(l,e)}d.options=d.setOptions=function(l){return _.setOptions(l),d.defaults=_.defaults,H(d.defaults),d};d.getDefaults=L;d.defaults=O;d.use=function(...l){return _.use(...l),d.defaults=_.defaults,H(d.defaults),d};d.walkTokens=function(l,e){return _.walkTokens(l,e)};d.parseInline=_.parseInline;d.Parser=R;d.parser=R.parse;d.Renderer=P;d.TextRenderer=S;d.Lexer=b;d.lexer=b.lex;d.Tokenizer=y;d.Hooks=$;d.parse=d;d.options;d.setOptions;d.use;d.walkTokens;d.parseInline;R.parse;b.lex; function extend (destination) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (source.hasOwnProperty(key)) destination[key] = source[key]; } } return destination } function repeat (character, count) { return Array(count + 1).join(character) } function trimLeadingNewlines (string) { return string.replace(/^\n*/, '') } function trimTrailingNewlines (string) { // avoid match-at-end regexp bottleneck, see #370 var indexEnd = string.length; while (indexEnd > 0 && string[indexEnd - 1] === '\n') indexEnd--; return string.substring(0, indexEnd) } var blockElements = [ 'ADDRESS', 'ARTICLE', 'ASIDE', 'AUDIO', 'BLOCKQUOTE', 'BODY', 'CANVAS', 'CENTER', 'DD', 'DIR', 'DIV', 'DL', 'DT', 'FIELDSET', 'FIGCAPTION', 'FIGURE', 'FOOTER', 'FORM', 'FRAMESET', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'HEADER', 'HGROUP', 'HR', 'HTML', 'ISINDEX', 'LI', 'MAIN', 'MENU', 'NAV', 'NOFRAMES', 'NOSCRIPT', 'OL', 'OUTPUT', 'P', 'PRE', 'SECTION', 'TABLE', 'TBODY', 'TD', 'TFOOT', 'TH', 'THEAD', 'TR', 'UL' ]; function isBlock (node) { return is(node, blockElements) } var voidElements = [ 'AREA', 'BASE', 'BR', 'COL', 'COMMAND', 'EMBED', 'HR', 'IMG', 'INPUT', 'KEYGEN', 'LINK', 'META', 'PARAM', 'SOURCE', 'TRACK', 'WBR' ]; function isVoid (node) { return is(node, voidElements) } function hasVoid (node) { return has(node, voidElements) } var meaningfulWhenBlankElements = [ 'A', 'TABLE', 'THEAD', 'TBODY', 'TFOOT', 'TH', 'TD', 'IFRAME', 'SCRIPT', 'AUDIO', 'VIDEO' ]; function isMeaningfulWhenBlank (node) { return is(node, meaningfulWhenBlankElements) } function hasMeaningfulWhenBlank (node) { return has(node, meaningfulWhenBlankElements) } function is (node, tagNames) { return tagNames.indexOf(node.nodeName) >= 0 } function has (node, tagNames) { return ( node.getElementsByTagName && tagNames.some(function (tagName) { return node.getElementsByTagName(tagName).length }) ) } var rules = {}; rules.paragraph = { filter: 'p', replacement: function (content) { return '\n\n' + content + '\n\n' } }; rules.lineBreak = { filter: 'br', replacement: function (content, node, options) { return options.br + '\n' } }; rules.heading = { filter: ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'], replacement: function (content, node, options) { var hLevel = Number(node.nodeName.charAt(1)); if (options.headingStyle === 'setext' && hLevel < 3) { var underline = repeat((hLevel === 1 ? '=' : '-'), content.length); return ( '\n\n' + content + '\n' + underline + '\n\n' ) } else { return '\n\n' + repeat('#', hLevel) + ' ' + content + '\n\n' } } }; rules.blockquote = { filter: 'blockquote', replacement: function (content) { content = content.replace(/^\n+|\n+$/g, ''); content = content.replace(/^/gm, '> '); return '\n\n' + content + '\n\n' } }; rules.list = { filter: ['ul', 'ol'], replacement: function (content, node) { var parent = node.parentNode; if (parent.nodeName === 'LI' && parent.lastElementChild === node) { return '\n' + content } else { return '\n\n' + content + '\n\n' } } }; rules.listItem = { filter: 'li', replacement: function (content, node, options) { var prefix = options.bulletListMarker + ' '; var parent = node.parentNode; if (parent.nodeName === 'OL') { var start = parent.getAttribute('start'); var index = Array.prototype.indexOf.call(parent.children, node); prefix = (start ? Number(start) + index : index + 1) + '. '; } content = content .replace(/^\n+/, '') // remove leading newlines .replace(/\n+$/, '\n') // replace trailing newlines with just a single one .replace(/\n/gm, '\n' + ' '.repeat(prefix.length)); // indent return ( prefix + content + (node.nextSibling && !/\n$/.test(content) ? '\n' : '') ) } }; rules.indentedCodeBlock = { filter: function (node, options) { return ( options.codeBlockStyle === 'indented' && node.nodeName === 'PRE' && node.firstChild && node.firstChild.nodeName === 'CODE' ) }, replacement: function (content, node, options) { return ( '\n\n ' + node.firstChild.textContent.replace(/\n/g, '\n ') + '\n\n' ) } }; rules.fencedCodeBlock = { filter: function (node, options) { return ( options.codeBlockStyle === 'fenced' && node.nodeName === 'PRE' && node.firstChild && node.firstChild.nodeName === 'CODE' ) }, replacement: function (content, node, options) { var className = node.firstChild.getAttribute('class') || ''; var language = (className.match(/language-(\S+)/) || [null, ''])[1]; var code = node.firstChild.textContent; var fenceChar = options.fence.charAt(0); var fenceSize = 3; var fenceInCodeRegex = new RegExp('^' + fenceChar + '{3,}', 'gm'); var match; while ((match = fenceInCodeRegex.exec(code))) { if (match[0].length >= fenceSize) { fenceSize = match[0].length + 1; } } var fence = repeat(fenceChar, fenceSize); return ( '\n\n' + fence + language + '\n' + code.replace(/\n$/, '') + '\n' + fence + '\n\n' ) } }; rules.horizontalRule = { filter: 'hr', replacement: function (content, node, options) { return '\n\n' + options.hr + '\n\n' } }; rules.inlineLink = { filter: function (node, options) { return ( options.linkStyle === 'inlined' && node.nodeName === 'A' && node.getAttribute('href') ) }, replacement: function (content, node) { var href = node.getAttribute('href'); if (href) href = href.replace(/([()])/g, '\\$1'); var title = cleanAttribute(node.getAttribute('title')); if (title) title = ' "' + title.replace(/"/g, '\\"') + '"'; return '[' + content + '](' + href + title + ')' } }; rules.referenceLink = { filter: function (node, options) { return ( options.linkStyle === 'referenced' && node.nodeName === 'A' && node.getAttribute('href') ) }, replacement: function (content, node, options) { var href = node.getAttribute('href'); var title = cleanAttribute(node.getAttribute('title')); if (title) title = ' "' + title + '"'; var replacement; var reference; switch (options.linkReferenceStyle) { case 'collapsed': replacement = '[' + content + '][]'; reference = '[' + content + ']: ' + href + title; break case 'shortcut': replacement = '[' + content + ']'; reference = '[' + content + ']: ' + href + title; break default: var id = this.references.length + 1; replacement = '[' + content + '][' + id + ']'; reference = '[' + id + ']: ' + href + title; } this.references.push(reference); return replacement }, references: [], append: function (options) { var references = ''; if (this.references.length) { references = '\n\n' + this.references.join('\n') + '\n\n'; this.references = []; // Reset references } return references } }; rules.emphasis = { filter: ['em', 'i'], replacement: function (content, node, options) { if (!content.trim()) return '' return options.emDelimiter + content + options.emDelimiter } }; rules.strong = { filter: ['strong', 'b'], replacement: function (content, node, options) { if (!content.trim()) return '' return options.strongDelimiter + content + options.strongDelimiter } }; rules.code = { filter: function (node) { var hasSiblings = node.previousSibling || node.nextSibling; var isCodeBlock = node.parentNode.nodeName === 'PRE' && !hasSiblings; return node.nodeName === 'CODE' && !isCodeBlock }, replacement: function (content) { if (!content) return '' content = content.replace(/\r?\n|\r/g, ' '); var extraSpace = /^`|^ .*?[^ ].* $|`$/.test(content) ? ' ' : ''; var delimiter = '`'; var matches = content.match(/`+/gm) || []; while (matches.indexOf(delimiter) !== -1) delimiter = delimiter + '`'; return delimiter + extraSpace + content + extraSpace + delimiter } }; rules.image = { filter: 'img', replacement: function (content, node) { var alt = cleanAttribute(node.getAttribute('alt')); var src = node.getAttribute('src') || ''; var title = cleanAttribute(node.getAttribute('title')); var titlePart = title ? ' "' + title + '"' : ''; return src ? '![' + alt + ']' + '(' + src + titlePart + ')' : '' } }; function cleanAttribute (attribute) { return attribute ? attribute.replace(/(\n+\s*)+/g, '\n') : '' } /** * Manages a collection of rules used to convert HTML to Markdown */ function Rules (options) { this.options = options; this._keep = []; this._remove = []; this.blankRule = { replacement: options.blankReplacement }; this.keepReplacement = options.keepReplacement; this.defaultRule = { replacement: options.defaultReplacement }; this.array = []; for (var key in options.rules) this.array.push(options.rules[key]); } Rules.prototype = { add: function (key, rule) { this.array.unshift(rule); }, keep: function (filter) { this._keep.unshift({ filter: filter, replacement: this.keepReplacement }); }, remove: function (filter) { this._remove.unshift({ filter: filter, replacement: function () { return '' } }); }, forNode: function (node) { if (node.isBlank) return this.blankRule var rule; if ((rule = findRule(this.array, node, this.options))) return rule if ((rule = findRule(this._keep, node, this.options))) return rule if ((rule = findRule(this._remove, node, this.options))) return rule return this.defaultRule }, forEach: function (fn) { for (var i = 0; i < this.array.length; i++) fn(this.array[i], i); } }; function findRule (rules, node, options) { for (var i = 0; i < rules.length; i++) { var rule = rules[i]; if (filterValue(rule, node, options)) return rule } return void 0 } function filterValue (rule, node, options) { var filter = rule.filter; if (typeof filter === 'string') { if (filter === node.nodeName.toLowerCase()) return true } else if (Array.isArray(filter)) { if (filter.indexOf(node.nodeName.toLowerCase()) > -1) return true } else if (typeof filter === 'function') { if (filter.call(rule, node, options)) return true } else { throw new TypeError('`filter` needs to be a string, array, or function') } } /** * The collapseWhitespace function is adapted from collapse-whitespace * by Luc Thevenard. * * The MIT License (MIT) * * Copyright (c) 2014 Luc Thevenard * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ /** * collapseWhitespace(options) removes extraneous whitespace from an the given element. * * @param {Object} options */ function collapseWhitespace (options) { var element = options.element; var isBlock = options.isBlock; var isVoid = options.isVoid; var isPre = options.isPre || function (node) { return node.nodeName === 'PRE' }; if (!element.firstChild || isPre(element)) return var prevText = null; var keepLeadingWs = false; var prev = null; var node = next(prev, element, isPre); while (node !== element) { if (node.nodeType === 3 || node.nodeType === 4) { // Node.TEXT_NODE or Node.CDATA_SECTION_NODE var text = node.data.replace(/[ \r\n\t]+/g, ' '); if ((!prevText || / $/.test(prevText.data)) && !keepLeadingWs && text[0] === ' ') { text = text.substr(1); } // `text` might be empty at this point. if (!text) { node = remove(node); continue } node.data = text; prevText = node; } else if (node.nodeType === 1) { // Node.ELEMENT_NODE if (isBlock(node) || node.nodeName === 'BR') { if (prevText) { prevText.data = prevText.data.replace(/ $/, ''); } prevText = null; keepLeadingWs = false; } else if (isVoid(node) || isPre(node)) { // Avoid trimming space around non-block, non-BR void elements and inline PRE. prevText = null; keepLeadingWs = true; } else if (prevText) { // Drop protection if set previously. keepLeadingWs = false; } } else { node = remove(node); continue } var nextNode = next(prev, node, isPre); prev = node; node = nextNode; } if (prevText) { prevText.data = prevText.data.replace(/ $/, ''); if (!prevText.data) { remove(prevText); } } } /** * remove(node) removes the given node from the DOM and returns the * next node in the sequence. * * @param {Node} node * @return {Node} node */ function remove (node) { var next = node.nextSibling || node.parentNode; node.parentNode.removeChild(node); return next } /** * next(prev, current, isPre) returns the next node in the sequence, given the * current and previous nodes. * * @param {Node} prev * @param {Node} current * @param {Function} isPre * @return {Node} */ function next (prev, current, isPre) { if ((prev && prev.parentNode === current) || isPre(current)) { return current.nextSibling || current.parentNode } return current.firstChild || current.nextSibling || current.parentNode } /* * Set up window for Node.js */ var root = (typeof window !== 'undefined' ? window : {}); /* * Parsing HTML strings */ function canParseHTMLNatively () { var Parser = root.DOMParser; var canParse = false; // Adapted from https://gist.github.com/1129031 // Firefox/Opera/IE throw errors on unsupported types try { // WebKit returns null on unsupported types if (new Parser().parseFromString('', 'text/html')) { canParse = true; } } catch (e) {} return canParse } function createHTMLParser () { var Parser = function () {}; { var domino = require('@mixmark-io/domino'); Parser.prototype.parseFromString = function (string) { return domino.createDocument(string) }; } return Parser } var HTMLParser = canParseHTMLNatively() ? root.DOMParser : createHTMLParser(); function RootNode (input, options) { var root; if (typeof input === 'string') { var doc = htmlParser().parseFromString( // DOM parsers arrange elements in the and . // Wrapping in a custom element ensures elements are reliably arranged in // a single element. '' + input + '', 'text/html' ); root = doc.getElementById('turndown-root'); } else { root = input.cloneNode(true); } collapseWhitespace({ element: root, isBlock: isBlock, isVoid: isVoid, isPre: options.preformattedCode ? isPreOrCode : null }); return root } var _htmlParser; function htmlParser () { _htmlParser = _htmlParser || new HTMLParser(); return _htmlParser } function isPreOrCode (node) { return node.nodeName === 'PRE' || node.nodeName === 'CODE' } function Node (node, options) { node.isBlock = isBlock(node); node.isCode = node.nodeName === 'CODE' || node.parentNode.isCode; node.isBlank = isBlank(node); node.flankingWhitespace = flankingWhitespace(node, options); return node } function isBlank (node) { return ( !isVoid(node) && !isMeaningfulWhenBlank(node) && /^\s*$/i.test(node.textContent) && !hasVoid(node) && !hasMeaningfulWhenBlank(node) ) } function flankingWhitespace (node, options) { if (node.isBlock || (options.preformattedCode && node.isCode)) { return { leading: '', trailing: '' } } var edges = edgeWhitespace(node.textContent); // abandon leading ASCII WS if left-flanked by ASCII WS if (edges.leadingAscii && isFlankedByWhitespace('left', node, options)) { edges.leading = edges.leadingNonAscii; } // abandon trailing ASCII WS if right-flanked by ASCII WS if (edges.trailingAscii && isFlankedByWhitespace('right', node, options)) { edges.trailing = edges.trailingNonAscii; } return { leading: edges.leading, trailing: edges.trailing } } function edgeWhitespace (string) { var m = string.match(/^(([ \t\r\n]*)(\s*))(?:(?=\S)[\s\S]*\S)?((\s*?)([ \t\r\n]*))$/); return { leading: m[1], // whole string for whitespace-only strings leadingAscii: m[2], leadingNonAscii: m[3], trailing: m[4], // empty for whitespace-only strings trailingNonAscii: m[5], trailingAscii: m[6] } } function isFlankedByWhitespace (side, node, options) { var sibling; var regExp; var isFlanked; if (side === 'left') { sibling = node.previousSibling; regExp = / $/; } else { sibling = node.nextSibling; regExp = /^ /; } if (sibling) { if (sibling.nodeType === 3) { isFlanked = regExp.test(sibling.nodeValue); } else if (options.preformattedCode && sibling.nodeName === 'CODE') { isFlanked = false; } else if (sibling.nodeType === 1 && !isBlock(sibling)) { isFlanked = regExp.test(sibling.textContent); } } return isFlanked } var reduce = Array.prototype.reduce; var escapes = [ [/\\/g, '\\\\'], [/\*/g, '\\*'], [/^-/g, '\\-'], [/^\+ /g, '\\+ '], [/^(=+)/g, '\\$1'], [/^(#{1,6}) /g, '\\$1 '], [/`/g, '\\`'], [/^~~~/g, '\\~~~'], [/\[/g, '\\['], [/\]/g, '\\]'], [/^>/g, '\\>'], [/_/g, '\\_'], [/^(\d+)\. /g, '$1\\. '] ]; function TurndownService (options) { if (!(this instanceof TurndownService)) return new TurndownService(options) var defaults = { rules: rules, headingStyle: 'setext', hr: '* * *', bulletListMarker: '*', codeBlockStyle: 'indented', fence: '```', emDelimiter: '_', strongDelimiter: '**', linkStyle: 'inlined', linkReferenceStyle: 'full', br: ' ', preformattedCode: false, blankReplacement: function (content, node) { return node.isBlock ? '\n\n' : '' }, keepReplacement: function (content, node) { return node.isBlock ? '\n\n' + node.outerHTML + '\n\n' : node.outerHTML }, defaultReplacement: function (content, node) { return node.isBlock ? '\n\n' + content + '\n\n' : content } }; this.options = extend({}, defaults, options); this.rules = new Rules(this.options); } TurndownService.prototype = { /** * The entry point for converting a string or DOM node to Markdown * @public * @param {String|HTMLElement} input The string or DOM node to convert * @returns A Markdown representation of the input * @type String */ turndown: function (input) { if (!canConvert(input)) { throw new TypeError( input + ' is not a string, or an element/document/fragment node.' ) } if (input === '') return '' var output = process.call(this, new RootNode(input, this.options)); return postProcess.call(this, output) }, /** * Add one or more plugins * @public * @param {Function|Array} plugin The plugin or array of plugins to add * @returns The Turndown instance for chaining * @type Object */ use: function (plugin) { if (Array.isArray(plugin)) { for (var i = 0; i < plugin.length; i++) this.use(plugin[i]); } else if (typeof plugin === 'function') { plugin(this); } else { throw new TypeError('plugin must be a Function or an Array of Functions') } return this }, /** * Adds a rule * @public * @param {String} key The unique key of the rule * @param {Object} rule The rule * @returns The Turndown instance for chaining * @type Object */ addRule: function (key, rule) { this.rules.add(key, rule); return this }, /** * Keep a node (as HTML) that matches the filter * @public * @param {String|Array|Function} filter The unique key of the rule * @returns The Turndown instance for chaining * @type Object */ keep: function (filter) { this.rules.keep(filter); return this }, /** * Remove a node that matches the filter * @public * @param {String|Array|Function} filter The unique key of the rule * @returns The Turndown instance for chaining * @type Object */ remove: function (filter) { this.rules.remove(filter); return this }, /** * Escapes Markdown syntax * @public * @param {String} string The string to escape * @returns A string with Markdown syntax escaped * @type String */ escape: function (string) { return escapes.reduce(function (accumulator, escape) { return accumulator.replace(escape[0], escape[1]) }, string) } }; /** * Reduces a DOM node down to its Markdown string equivalent * @private * @param {HTMLElement} parentNode The node to convert * @returns A Markdown representation of the node * @type String */ function process (parentNode) { var self = this; return reduce.call(parentNode.childNodes, function (output, node) { node = new Node(node, self.options); var replacement = ''; if (node.nodeType === 3) { replacement = node.isCode ? node.nodeValue : self.escape(node.nodeValue); } else if (node.nodeType === 1) { replacement = replacementForNode.call(self, node); } return join(output, replacement) }, '') } /** * Appends strings as each rule requires and trims the output * @private * @param {String} output The conversion output * @returns A trimmed version of the ouput * @type String */ function postProcess (output) { var self = this; this.rules.forEach(function (rule) { if (typeof rule.append === 'function') { output = join(output, rule.append(self.options)); } }); return output.replace(/^[\t\r\n]+/, '').replace(/[\t\r\n\s]+$/, '') } /** * Converts an element node to its Markdown equivalent * @private * @param {HTMLElement} node The node to convert * @returns A Markdown representation of the node * @type String */ function replacementForNode (node) { var rule = this.rules.forNode(node); var content = process.call(this, node); var whitespace = node.flankingWhitespace; if (whitespace.leading || whitespace.trailing) content = content.trim(); return ( whitespace.leading + rule.replacement(content, node, this.options) + whitespace.trailing ) } /** * Joins replacement to the current output with appropriate number of new lines * @private * @param {String} output The current conversion output * @param {String} replacement The string to append to the output * @returns Joined output * @type String */ function join (output, replacement) { var s1 = trimTrailingNewlines(output); var s2 = trimLeadingNewlines(replacement); var nls = Math.max(output.length - s1.length, replacement.length - s2.length); var separator = '\n\n'.substring(0, nls); return s1 + separator + s2 } /** * Determines whether an input can be converted * @private * @param {String|HTMLElement} input Describe this parameter * @returns Describe what it returns * @type String|Object|Array|Boolean|Number */ function canConvert (input) { return ( input != null && ( typeof input === 'string' || (input.nodeType && ( input.nodeType === 1 || input.nodeType === 9 || input.nodeType === 11 )) ) ) } /** * Markdown conversion utilities using Marked and Turndown */ /** * MarkdownConverter - Handles bidirectional HTML ↔ Markdown conversion */ class MarkdownConverter { constructor() { this.initializeMarked(); this.initializeTurndown(); } /** * Configure marked for HTML output - MINIMAL MODE * Only supports: **bold**, *italic*, and [links](url) * Matches server-side goldmark configuration */ initializeMarked() { d.setOptions({ gfm: false, // Disable GFM to match server minimal mode breaks: true, // Convert \n to
    (matches server) pedantic: false, // Don't be overly strict sanitize: false, // Allow HTML (we control the input) smartLists: false, // Disable lists (not supported on server) smartypants: false // Don't convert quotes/dashes }); // Override renderers to restrict to minimal feature set d.use({ renderer: { // Disable headings - treat as plain text heading(text, level) { return text; }, // Disable lists - treat as plain text list(body, ordered, start) { return body.replace(/<\/?li>/g, ''); }, listitem(text) { return text + '\n'; }, // Disable code blocks - treat as plain text code(code, language) { return code; }, blockquote(quote) { return quote; // Disable blockquotes - treat as plain text }, // Disable horizontal rules hr() { return ''; }, // Disable tables table(header, body) { return header + body; }, tablecell(content, flags) { return content; }, tablerow(content) { return content; } } }); } /** * Configure turndown for markdown output - MINIMAL MODE * Only supports: **bold**, *italic*, and [links](url) * Matches server-side goldmark configuration */ initializeTurndown() { this.turndown = new TurndownService({ // Minimal configuration - only basic formatting headingStyle: 'atx', // # headers (but will be disabled) hr: '---', // horizontal rule (but will be disabled) bulletListMarker: '-', // bullet list (but will be disabled) codeBlockStyle: 'fenced', // code blocks (but will be disabled) fence: '```', // fence marker (but will be disabled) emDelimiter: '*', // *italic* - matches server strongDelimiter: '**', // **bold** - matches server linkStyle: 'inlined', // [text](url) - matches server linkReferenceStyle: 'full' // full reference links }); // Add custom rules for better conversion this.addTurndownRules(); } /** * Add custom turndown rules - MINIMAL MODE * Only supports: **bold**, *italic*, and [links](url) * Disables all other formatting to match server */ addTurndownRules() { // Handle paragraph spacing properly - ensure double newlines between paragraphs this.turndown.addRule('paragraph', { filter: 'p', replacement: function (content) { if (!content.trim()) return ''; return content.trim() + '\n\n'; } }); // Handle bold text in markdown - keep this (supported) this.turndown.addRule('bold', { filter: ['strong', 'b'], replacement: function (content) { if (!content.trim()) return ''; return '**' + content + '**'; } }); // Handle italic text in markdown - keep this (supported) this.turndown.addRule('italic', { filter: ['em', 'i'], replacement: function (content) { if (!content.trim()) return ''; return '*' + content + '*'; } }); // DISABLE unsupported features - convert to plain text this.turndown.addRule('disableHeadings', { filter: ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'], replacement: function (content) { return content; // Just return text content, no # markup } }); this.turndown.addRule('disableLists', { filter: ['ul', 'ol', 'li'], replacement: function (content) { return content; // Just return text content, no list markup } }); this.turndown.addRule('disableCode', { filter: ['pre', 'code'], replacement: function (content) { return content; // Just return text content, no code markup } }); this.turndown.addRule('disableBlockquotes', { filter: 'blockquote', replacement: function (content) { return content; // Just return text content, no > markup } }); this.turndown.addRule('disableHR', { filter: 'hr', replacement: function () { return ''; // Remove horizontal rules entirely } }); } /** * Convert HTML to Markdown * @param {string} html - HTML string to convert * @returns {string} - Markdown string */ htmlToMarkdown(html) { if (!html || html.trim() === '') { return ''; } try { const markdown = this.turndown.turndown(html); // Clean up and normalize newlines for proper paragraph separation return markdown .replace(/\n{3,}/g, '\n\n') // Replace 3+ newlines with 2 .replace(/^\n+|\n+$/g, '') // Remove leading/trailing newlines .trim(); // Remove other whitespace } catch (error) { console.warn('HTML to Markdown conversion failed:', error); // Fallback: extract text content const tempDiv = document.createElement('div'); tempDiv.innerHTML = html; return tempDiv.textContent || tempDiv.innerText || ''; } } /** * Convert Markdown to HTML * @param {string} markdown - Markdown string to convert * @returns {string} - HTML string */ markdownToHtml(markdown) { if (!markdown || markdown.trim() === '') { return ''; } try { const html = d(markdown); return html; } catch (error) { console.warn('Markdown to HTML conversion failed:', error); // Fallback: convert line breaks to paragraphs return markdown .split(/\n\s*\n/) .filter(p => p.trim()) .map(p => `

    ${p.trim()}

    `) .join(''); } } /** * Extract HTML content from a group of elements * @param {HTMLElement[]} elements - Array of DOM elements * @returns {string} - Combined HTML content */ extractGroupHTML(elements) { const htmlParts = []; elements.forEach(element => { // Wrap inner content in paragraph tags to preserve structure const html = element.innerHTML.trim(); if (html) { // If element is already a paragraph, use its outer HTML if (element.tagName.toLowerCase() === 'p') { htmlParts.push(element.outerHTML); } else { // Wrap in paragraph tags htmlParts.push(`

    ${html}

    `); } } }); return htmlParts.join('\n'); } /** * Convert HTML content from group elements to markdown * @param {HTMLElement[]} elements - Array of DOM elements * @returns {string} - Markdown representation */ extractGroupMarkdown(elements) { const html = this.extractGroupHTML(elements); const markdown = this.htmlToMarkdown(html); return markdown; } /** * Update group elements with markdown content * @param {HTMLElement[]} elements - Array of DOM elements to update * @param {string} markdown - Markdown content to render */ updateGroupElements(elements, markdown) { const html = this.markdownToHtml(markdown); // Split HTML into paragraphs const tempDiv = document.createElement('div'); tempDiv.innerHTML = html; const paragraphs = Array.from(tempDiv.querySelectorAll('p, div, h1, h2, h3, h4, h5, h6')); // Handle case where we have more/fewer paragraphs than elements const maxCount = Math.max(elements.length, paragraphs.length); for (let i = 0; i < maxCount; i++) { if (i < elements.length && i < paragraphs.length) { // Update existing element with corresponding paragraph elements[i].innerHTML = paragraphs[i].innerHTML; } else if (i < elements.length) { // More elements than paragraphs - clear extra elements elements[i].innerHTML = ''; } else if (i < paragraphs.length) { // More paragraphs than elements - create new element const newElement = document.createElement('p'); newElement.innerHTML = paragraphs[i].innerHTML; // Insert after the last existing element const lastElement = elements[elements.length - 1]; lastElement.parentNode.insertBefore(newElement, lastElement.nextSibling); elements.push(newElement); // Add to our elements array for future updates } } } } // Export singleton instance const markdownConverter = new MarkdownConverter(); /** * Previewer - Handles live preview for all content types */ class Previewer { constructor() { this.previewTimeout = null; this.activeContext = null; this.resizeObserver = null; this.onHeightChange = null; } /** * Set the active editing context for preview */ setActiveContext(context) { this.clearPreview(); this.activeContext = context; this.startResizeObserver(); } /** * Schedule a preview update with debouncing */ schedulePreview(context, content) { // Clear existing timeout if (this.previewTimeout) { clearTimeout(this.previewTimeout); } // Schedule new preview with 500ms debounce this.previewTimeout = setTimeout(() => { this.updatePreview(context, content); }, 500); } /** * Update preview with new content */ updatePreview(context, content) { // Store original content if first preview if (!context.originalContent) { context.storeOriginalContent(); } // Apply preview content to elements this.applyPreviewContent(context, content); context.applyPreviewStyling(); } /** * Apply preview content to context elements */ applyPreviewContent(context, content) { if (context.elements.length === 1) { const element = context.elements[0]; // Handle links specially if (element.tagName.toLowerCase() === 'a') { if (typeof content === 'object') { // Update link text (markdown to HTML) if (content.text !== undefined) { const html = markdownConverter.markdownToHtml(content.text); element.innerHTML = html; } // Update link URL if (content.url !== undefined && content.url.trim()) { element.href = content.url; } } else if (content && content.trim()) { // Just markdown content for link text const html = markdownConverter.markdownToHtml(content); element.innerHTML = html; } return; } // Regular single element if (content && content.trim()) { const html = markdownConverter.markdownToHtml(content); element.innerHTML = html; } } else { // Multiple elements - use group update if (content && content.trim()) { markdownConverter.updateGroupElements(context.elements, content); } } } /** * Clear all preview state and restore original content */ clearPreview() { if (this.activeContext) { this.activeContext.restoreOriginalContent(); this.activeContext.removePreviewStyling(); this.activeContext = null; } if (this.previewTimeout) { clearTimeout(this.previewTimeout); this.previewTimeout = null; } this.stopResizeObserver(); } /** * Start observing element size changes for modal repositioning */ startResizeObserver() { this.stopResizeObserver(); if (this.activeContext) { this.resizeObserver = new ResizeObserver(() => { // Handle height changes for modal repositioning if (this.onHeightChange) { this.onHeightChange(this.activeContext.primaryElement); } }); // Observe all elements in the context this.activeContext.elements.forEach(el => { this.resizeObserver.observe(el); }); } } /** * Stop observing element size changes */ stopResizeObserver() { if (this.resizeObserver) { this.resizeObserver.disconnect(); this.resizeObserver = null; } } /** * Set callback for height changes (for modal repositioning) */ setHeightChangeCallback(callback) { this.onHeightChange = callback; } /** * Get unique element ID for tracking */ getElementId(element) { if (!element._insertrId) { element._insertrId = 'insertr_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9); } return element._insertrId; } } /** * Editor - Handles all content types with markdown-first approach */ class Editor { constructor() { this.currentOverlay = null; this.previewer = new Previewer(); } /** * Edit any content element with markdown interface * @param {Object} meta - Element metadata {element, contentId, contentType} * @param {string|Object} currentContent - Current content value * @param {Function} onSave - Save callback * @param {Function} onCancel - Cancel callback */ edit(meta, currentContent, onSave, onCancel) { const { element } = meta; // Handle both single elements and groups uniformly const elements = Array.isArray(element) ? element : [element]; const context = new EditContext(elements, currentContent); // Close any existing editor this.close(); // Create editor form const form = this.createForm(context, meta); const overlay = this.createOverlay(form); // Position relative to primary element this.positionForm(context.primaryElement, overlay); // Setup event handlers this.setupEventHandlers(form, overlay, context, { onSave, onCancel }); // Show editor document.body.appendChild(overlay); this.currentOverlay = overlay; // Focus textarea const textarea = form.querySelector('textarea'); if (textarea) { setTimeout(() => textarea.focus(), 100); } return overlay; } /** * Create editing form for any content type */ createForm(context, meta) { const config = this.getFieldConfig(context); const currentContent = context.extractContent(); const form = document.createElement('div'); form.className = 'insertr-edit-form'; // Build form HTML let formHTML = `
    ${config.label}
    `; // Markdown textarea (always present) formHTML += this.createMarkdownField(config, currentContent); // URL field (for links only) if (config.includeUrl) { formHTML += this.createUrlField(currentContent); } // Form actions formHTML += `
    `; form.innerHTML = formHTML; return form; } /** * Get field configuration for any element type (markdown-first) */ getFieldConfig(context) { const elementCount = context.elements.length; const primaryElement = context.primaryElement; const isLink = primaryElement.tagName.toLowerCase() === 'a'; // Multi-element groups if (elementCount > 1) { return { type: 'markdown', includeUrl: false, label: `Group Content (${elementCount} elements)`, rows: Math.max(8, elementCount * 2), placeholder: 'Edit all content together using markdown...' }; } // Single elements - all get markdown by default const tag = primaryElement.tagName.toLowerCase(); const baseConfig = { type: 'markdown', includeUrl: isLink, placeholder: 'Enter content using markdown...' }; // Customize by element type switch (tag) { case 'h1': return { ...baseConfig, label: 'Main Headline', rows: 1, placeholder: 'Enter main headline...' }; case 'h2': return { ...baseConfig, label: 'Subheading', rows: 1, placeholder: 'Enter subheading...' }; case 'h3': case 'h4': case 'h5': case 'h6': return { ...baseConfig, label: 'Heading', rows: 2, placeholder: 'Enter heading (markdown supported)...' }; case 'p': return { ...baseConfig, label: 'Content', rows: 4, placeholder: 'Enter content using markdown...' }; case 'span': return { ...baseConfig, label: 'Text', rows: 2, placeholder: 'Enter text (markdown supported)...' }; case 'button': return { ...baseConfig, label: 'Button Text', rows: 1, placeholder: 'Enter button text...' }; case 'a': return { ...baseConfig, label: 'Link', rows: 2, placeholder: 'Enter link text (markdown supported)...' }; default: return { ...baseConfig, label: 'Content', rows: 3, placeholder: 'Enter content using markdown...' }; } } /** * Create markdown textarea field */ createMarkdownField(config, content) { const textContent = typeof content === 'object' ? content.text || '' : content; return `
    Supports Markdown formatting (bold, italic, links, etc.)
    `; } /** * Create URL field for links */ createUrlField(content) { const url = typeof content === 'object' ? content.url || '' : ''; return `
    `; } /** * Setup event handlers */ setupEventHandlers(form, overlay, context, { onSave, onCancel }) { const textarea = form.querySelector('textarea'); const urlInput = form.querySelector('input[name="url"]'); const saveBtn = form.querySelector('.insertr-btn-save'); const cancelBtn = form.querySelector('.insertr-btn-cancel'); const historyBtn = form.querySelector('.insertr-btn-history'); // Initialize previewer this.previewer.setActiveContext(context); // Setup live preview for content changes if (textarea) { textarea.addEventListener('input', () => { const content = this.extractFormData(form); this.previewer.schedulePreview(context, content); }); } // Setup live preview for URL changes (links only) if (urlInput) { urlInput.addEventListener('input', () => { const content = this.extractFormData(form); this.previewer.schedulePreview(context, content); }); } // Save handler if (saveBtn) { saveBtn.addEventListener('click', () => { const content = this.extractFormData(form); // Apply final content to elements context.applyContent(content); // Update stored original content to match current state // This makes the saved content the new baseline for future edits context.updateOriginalContent(); // Clear preview styling (won't restore content since original matches current) this.previewer.clearPreview(); // Callback with the content onSave(content); this.close(); }); } // Cancel handler if (cancelBtn) { cancelBtn.addEventListener('click', () => { this.previewer.clearPreview(); onCancel(); this.close(); }); } // History handler if (historyBtn) { historyBtn.addEventListener('click', () => { const contentId = historyBtn.getAttribute('data-content-id'); console.log('Version history not implemented yet for:', contentId); // TODO: Implement version history integration }); } // ESC key handler const keyHandler = (e) => { if (e.key === 'Escape') { this.previewer.clearPreview(); onCancel(); this.close(); document.removeEventListener('keydown', keyHandler); } }; document.addEventListener('keydown', keyHandler); // Click outside handler overlay.addEventListener('click', (e) => { if (e.target === overlay) { this.previewer.clearPreview(); onCancel(); this.close(); } }); } /** * Extract form data consistently */ extractFormData(form) { const textarea = form.querySelector('textarea[name="content"]'); const urlInput = form.querySelector('input[name="url"]'); const content = textarea ? textarea.value : ''; if (urlInput) { // Link content return { text: content, url: urlInput.value }; } // Regular content return content; } /** * Create overlay with backdrop */ createOverlay(form) { const overlay = document.createElement('div'); overlay.className = 'insertr-form-overlay'; overlay.appendChild(form); return overlay; } /** * Position form relative to primary element */ positionForm(element, overlay) { const rect = element.getBoundingClientRect(); const form = overlay.querySelector('.insertr-edit-form'); const viewportWidth = window.innerWidth; // Calculate optimal width let formWidth; if (viewportWidth < 768) { formWidth = Math.min(viewportWidth - 40, 500); } else { const minComfortableWidth = 600; const maxWidth = Math.min(viewportWidth * 0.9, 800); formWidth = Math.max(minComfortableWidth, Math.min(rect.width * 1.5, maxWidth)); } form.style.width = `${formWidth}px`; // Position below element const top = rect.bottom + window.scrollY + 10; const centerLeft = rect.left + window.scrollX + (rect.width / 2) - (formWidth / 2); const minLeft = 20; const maxLeft = window.innerWidth - formWidth - 20; const left = Math.max(minLeft, Math.min(centerLeft, maxLeft)); overlay.style.position = 'absolute'; overlay.style.top = `${top}px`; overlay.style.left = `${left}px`; overlay.style.zIndex = '10000'; // Ensure visibility this.ensureModalVisible(overlay); } /** * Ensure modal is visible by scrolling if needed */ ensureModalVisible(overlay) { requestAnimationFrame(() => { const modal = overlay.querySelector('.insertr-edit-form'); const modalRect = modal.getBoundingClientRect(); const viewportHeight = window.innerHeight; if (modalRect.bottom > viewportHeight) { const scrollAmount = modalRect.bottom - viewportHeight + 20; window.scrollBy({ top: scrollAmount, behavior: 'smooth' }); } }); } /** * Close current editor */ close() { if (this.previewer) { this.previewer.clearPreview(); } if (this.currentOverlay) { this.currentOverlay.remove(); this.currentOverlay = null; } } /** * Escape HTML to prevent XSS */ escapeHtml(text) { if (typeof text !== 'string') return ''; const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } } /** * EditContext - Represents content elements for editing */ class EditContext { constructor(elements, currentContent) { this.elements = elements; this.primaryElement = elements[0]; this.originalContent = null; this.currentContent = currentContent; } /** * Extract content from elements in markdown format */ extractContent() { if (this.elements.length === 1) { const element = this.elements[0]; // Handle links specially if (element.tagName.toLowerCase() === 'a') { return { text: markdownConverter.htmlToMarkdown(element.innerHTML), url: element.href }; } // Single element - convert to markdown return markdownConverter.htmlToMarkdown(element.innerHTML); } else { // Multiple elements - use group extraction return markdownConverter.extractGroupMarkdown(this.elements); } } /** * Apply content to elements from markdown/object */ applyContent(content) { if (this.elements.length === 1) { const element = this.elements[0]; // Handle links specially if (element.tagName.toLowerCase() === 'a' && typeof content === 'object') { element.innerHTML = markdownConverter.markdownToHtml(content.text || ''); if (content.url) { element.href = content.url; } return; } // Single element - convert markdown to HTML const html = markdownConverter.markdownToHtml(content); element.innerHTML = html; } else { // Multiple elements - use group update markdownConverter.updateGroupElements(this.elements, content); } } /** * Store original content for preview restoration */ storeOriginalContent() { this.originalContent = this.elements.map(el => ({ innerHTML: el.innerHTML, href: el.href // Store href for links })); } /** * Restore original content (for preview cancellation) */ restoreOriginalContent() { if (this.originalContent) { this.elements.forEach((el, index) => { if (this.originalContent[index] !== undefined) { el.innerHTML = this.originalContent[index].innerHTML; if (this.originalContent[index].href) { el.href = this.originalContent[index].href; } } }); } } /** * Update original content to match current element state (after save) * This makes the current content the new baseline for future cancellations */ updateOriginalContent() { this.originalContent = this.elements.map(el => ({ innerHTML: el.innerHTML, href: el.href // Store href for links })); } /** * Apply preview styling to all elements */ applyPreviewStyling() { this.elements.forEach(el => { el.classList.add('insertr-preview-active'); }); // Also apply to containers if they're groups if (this.primaryElement.classList.contains('insertr-group')) { this.primaryElement.classList.add('insertr-preview-active'); } } /** * Remove preview styling from all elements */ removePreviewStyling() { this.elements.forEach(el => { el.classList.remove('insertr-preview-active'); }); // Also remove from containers if (this.primaryElement.classList.contains('insertr-group')) { this.primaryElement.classList.remove('insertr-preview-active'); } } } /** * InsertrFormRenderer - Form renderer using markdown-first approach * Thin wrapper around the Editor system */ class InsertrFormRenderer { constructor(apiClient = null) { this.apiClient = apiClient; this.editor = new Editor(); this.setupStyles(); } /** * Show edit form for any content element * @param {Object} meta - Element metadata {element, contentId, contentType} * @param {string|Object} currentContent - Current content value * @param {Function} onSave - Save callback * @param {Function} onCancel - Cancel callback */ showEditForm(meta, currentContent, onSave, onCancel) { const { element } = meta; // Handle insertr-group elements by getting their viable children if (element.classList.contains('insertr-group')) { const children = this.getGroupChildren(element); const groupMeta = { ...meta, element: children }; return this.editor.edit(groupMeta, currentContent, onSave, onCancel); } // All other elements use the editor directly return this.editor.edit(meta, currentContent, onSave, onCancel); } /** * Get viable children from group element */ getGroupChildren(groupElement) { const children = []; for (const child of groupElement.children) { // Skip elements that don't have meaningful text content if (child.textContent.trim().length > 0) { children.push(child); } } return children; } /** * Close current form */ closeForm() { this.editor.close(); } /** * Show version history modal (placeholder for future implementation) */ async showVersionHistory(contentId, element, onRestore) { try { // Get version history from API const apiClient = this.getApiClient(); if (!apiClient) { console.warn('No API client configured for version history'); return; } const versions = await apiClient.getContentVersions(contentId); // Create version history modal const historyModal = this.createVersionHistoryModal(contentId, versions, onRestore); document.body.appendChild(historyModal); // Setup handlers this.setupVersionHistoryHandlers(historyModal, contentId); } catch (error) { console.error('Failed to load version history:', error); this.showVersionHistoryError('Failed to load version history. Please try again.'); } } /** * Create version history modal (simplified placeholder) */ createVersionHistoryModal(contentId, versions, onRestore) { const modal = document.createElement('div'); modal.className = 'insertr-version-modal'; let versionsHTML = ''; if (versions && versions.length > 0) { versionsHTML = versions.map((version, index) => `
    ${index === 0 ? 'Previous Version' : `Version ${versions.length - index}`} ${this.formatDate(version.created_at)} ${version.created_by ? `by ${version.created_by}` : ''}
    ${this.escapeHtml(this.truncateContent(version.value, 100))}
    `).join(''); } else { versionsHTML = '
    No previous versions found
    '; } modal.innerHTML = `

    Version History

    ${versionsHTML}
    `; return modal; } /** * Setup version history modal handlers */ setupVersionHistoryHandlers(modal, contentId) { const closeBtn = modal.querySelector('.insertr-btn-close'); const backdrop = modal.querySelector('.insertr-version-backdrop'); // Close handlers if (closeBtn) { closeBtn.addEventListener('click', () => modal.remove()); } backdrop.addEventListener('click', (e) => { if (e.target === backdrop) { modal.remove(); } }); // Restore handlers const restoreButtons = modal.querySelectorAll('.insertr-btn-restore'); restoreButtons.forEach(btn => { btn.addEventListener('click', async () => { const versionId = btn.getAttribute('data-version-id'); if (await this.confirmRestore()) { await this.restoreVersion(contentId, versionId); modal.remove(); this.closeForm(); } }); }); // View diff handlers const viewButtons = modal.querySelectorAll('.insertr-btn-view-diff'); viewButtons.forEach(btn => { btn.addEventListener('click', () => { const versionId = btn.getAttribute('data-version-id'); this.showVersionDetails(versionId); }); }); } /** * Helper methods for version history */ formatDate(dateString) { const date = new Date(dateString); const now = new Date(); const diff = now - date; // Less than 24 hours ago if (diff < 24 * 60 * 60 * 1000) { const hours = Math.floor(diff / (60 * 60 * 1000)); if (hours < 1) { const minutes = Math.floor(diff / (60 * 1000)); return `${minutes}m ago`; } return `${hours}h ago`; } // Less than 7 days ago if (diff < 7 * 24 * 60 * 60 * 1000) { const days = Math.floor(diff / (24 * 60 * 60 * 1000)); return `${days}d ago`; } // Older - show actual date return date.toLocaleDateString(); } truncateContent(content, maxLength) { if (content.length <= maxLength) return content; return content.substring(0, maxLength) + '...'; } async confirmRestore() { return confirm('Are you sure you want to restore this version? This will replace the current content.'); } async restoreVersion(contentId, versionId) { try { const apiClient = this.getApiClient(); await apiClient.rollbackContent(contentId, versionId); return true; } catch (error) { console.error('Failed to restore version:', error); alert('Failed to restore version. Please try again.'); return false; } } showVersionDetails(versionId) { // TODO: Implement detailed version view with diff alert(`Version details not implemented yet (Version ID: ${versionId})`); } showVersionHistoryError(message) { alert(message); } // Helper to get API client getApiClient() { return this.apiClient || window.insertrAPIClient || null; } /** * Escape HTML to prevent XSS */ escapeHtml(text) { if (typeof text !== 'string') return ''; const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } /** * Setup form styles (consolidated and simplified) */ setupStyles() { const styles = ` /* Overlay and Form Container */ .insertr-form-overlay { position: absolute; z-index: 10000; } .insertr-edit-form { background: white; border: 2px solid #007cba; border-radius: 8px; padding: 1rem; box-shadow: 0 8px 25px rgba(0,0,0,0.15); width: 100%; box-sizing: border-box; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; min-width: 600px; max-width: 800px; } /* Form Header */ .insertr-form-header { font-weight: 600; color: #1f2937; margin-bottom: 1rem; padding-bottom: 0.5rem; border-bottom: 1px solid #e5e7eb; font-size: 0.875rem; text-transform: uppercase; letter-spacing: 0.5px; } /* Form Groups and Fields */ .insertr-form-group { margin-bottom: 1rem; } .insertr-form-group:last-child { margin-bottom: 0; } .insertr-form-label { display: block; font-weight: 600; color: #374151; margin-bottom: 0.5rem; font-size: 0.875rem; } .insertr-form-input, .insertr-form-textarea { width: 100%; padding: 0.75rem; border: 1px solid #d1d5db; border-radius: 6px; font-family: inherit; font-size: 1rem; transition: border-color 0.2s, box-shadow 0.2s; box-sizing: border-box; } .insertr-form-input:focus, .insertr-form-textarea:focus { outline: none; border-color: #007cba; box-shadow: 0 0 0 3px rgba(0, 124, 186, 0.1); } /* Markdown Editor Styling */ .insertr-form-textarea { min-height: 120px; resize: vertical; font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace; } .insertr-markdown-editor { min-height: 200px; font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace; font-size: 0.9rem; line-height: 1.5; background-color: #f8fafc; } /* Form Actions */ .insertr-form-actions { display: flex; gap: 0.5rem; justify-content: flex-end; margin-top: 1rem; padding-top: 1rem; border-top: 1px solid #e5e7eb; } .insertr-btn-save { background: #10b981; color: white; border: none; padding: 0.5rem 1rem; border-radius: 6px; font-weight: 500; cursor: pointer; transition: background-color 0.2s; font-size: 0.875rem; } .insertr-btn-save:hover { background: #059669; } .insertr-btn-cancel { background: #6b7280; color: white; border: none; padding: 0.5rem 1rem; border-radius: 6px; font-weight: 500; cursor: pointer; transition: background-color 0.2s; font-size: 0.875rem; } .insertr-btn-cancel:hover { background: #4b5563; } .insertr-btn-history { background: #6f42c1; color: white; border: none; padding: 0.5rem 1rem; border-radius: 6px; font-weight: 500; cursor: pointer; transition: background-color 0.2s; font-size: 0.875rem; } .insertr-btn-history:hover { background: #5a359a; } .insertr-form-help { font-size: 0.75rem; color: #6b7280; margin-top: 0.25rem; } /* Live Preview Styles */ .insertr-preview-active { position: relative; background: rgba(0, 124, 186, 0.05) !important; outline: 2px solid #007cba !important; outline-offset: 2px; transition: all 0.3s ease; } .insertr-preview-active::after { content: "Preview"; position: absolute; top: -25px; left: 0; background: #007cba; color: white; padding: 2px 8px; border-radius: 3px; font-size: 0.75rem; font-weight: 500; z-index: 10001; white-space: nowrap; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; } /* Responsive Design */ @media (max-width: 768px) { .insertr-edit-form { min-width: 90vw; max-width: 90vw; } .insertr-preview-active::after { top: -20px; font-size: 0.7rem; padding: 1px 6px; } } /* Version History Modal Styles */ .insertr-version-modal { position: fixed; top: 0; left: 0; right: 0; bottom: 0; z-index: 10001; } .insertr-version-backdrop { position: absolute; top: 0; left: 0; right: 0; bottom: 0; background-color: rgba(0, 0, 0, 0.5); display: flex; align-items: center; justify-content: center; padding: 20px; } .insertr-version-content-modal { background: white; border-radius: 8px; max-width: 600px; width: 100%; max-height: 80vh; box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2); display: flex; flex-direction: column; } .insertr-version-header { padding: 20px 20px 0; border-bottom: 1px solid #eee; display: flex; justify-content: space-between; align-items: center; flex-shrink: 0; } .insertr-version-header h3 { margin: 0 0 20px; color: #333; font-size: 18px; } .insertr-btn-close { background: none; border: none; font-size: 24px; cursor: pointer; color: #666; padding: 0; width: 30px; height: 30px; display: flex; align-items: center; justify-content: center; } .insertr-btn-close:hover { color: #333; } .insertr-version-list { overflow-y: auto; padding: 20px; flex: 1; } .insertr-version-item { border: 1px solid #e1e5e9; border-radius: 6px; padding: 16px; margin-bottom: 12px; background: #f8f9fa; } .insertr-version-meta { display: flex; align-items: center; gap: 12px; margin-bottom: 8px; font-size: 13px; } .insertr-version-label { font-weight: 600; color: #0969da; } .insertr-version-date { color: #656d76; } .insertr-version-user { color: #656d76; } .insertr-version-content { margin-bottom: 12px; padding: 8px; background: white; border-radius: 4px; font-family: monospace; font-size: 14px; color: #24292f; white-space: pre-wrap; } .insertr-version-actions { display: flex; gap: 8px; } .insertr-btn-restore { background: #0969da; color: white; border: none; padding: 6px 12px; border-radius: 4px; cursor: pointer; font-size: 12px; font-weight: 500; } .insertr-btn-restore:hover { background: #0860ca; } .insertr-btn-view-diff { background: #f6f8fa; color: #24292f; border: 1px solid #d1d9e0; padding: 6px 12px; border-radius: 4px; cursor: pointer; font-size: 12px; font-weight: 500; } .insertr-btn-view-diff:hover { background: #f3f4f6; } .insertr-version-empty { text-align: center; color: #656d76; font-style: italic; padding: 40px 20px; } `; const styleSheet = document.createElement('style'); styleSheet.type = 'text/css'; styleSheet.innerHTML = styles; document.head.appendChild(styleSheet); } } /** * InsertrEditor - Visual editing functionality */ class InsertrEditor { constructor(core, auth, apiClient, options = {}) { this.core = core; this.auth = auth; this.apiClient = apiClient; this.options = options; this.isActive = false; this.formRenderer = new InsertrFormRenderer(apiClient); } start() { if (this.isActive) return; console.log('🚀 Starting Insertr Editor'); this.isActive = true; // Add editor styles this.addEditorStyles(); // Initialize all enhanced elements const elements = this.core.getAllElements(); console.log(`📝 Found ${elements.length} editable elements`); elements.forEach(meta => this.initializeElement(meta)); } initializeElement(meta) { const { element, contentId, contentType } = meta; // Add visual indicators element.style.cursor = 'pointer'; element.style.position = 'relative'; // Add interaction handlers this.addHoverEffects(element); this.addClickHandler(element, meta); } addHoverEffects(element) { element.addEventListener('mouseenter', () => { element.classList.add('insertr-editing-hover'); }); element.addEventListener('mouseleave', () => { element.classList.remove('insertr-editing-hover'); }); } addClickHandler(element, meta) { element.addEventListener('click', (e) => { // Only allow editing if authenticated and in edit mode if (!this.auth.isAuthenticated() || !this.auth.isEditMode()) { return; // Let normal click behavior happen } e.preventDefault(); this.openEditor(meta); }); } openEditor(meta) { const { element } = meta; const currentContent = this.extractCurrentContent(element); // Show professional form instead of prompt this.formRenderer.showEditForm( meta, currentContent, (formData) => this.handleSave(meta, formData), () => this.handleCancel(meta) ); } extractCurrentContent(element) { // For links, extract both text and URL if (element.tagName.toLowerCase() === 'a') { return { text: element.textContent.trim(), url: element.getAttribute('href') || '' }; } // For other elements, just return text content return element.textContent.trim(); } async handleSave(meta, formData) { console.log('💾 Saving content:', meta.contentId, formData); try { // Extract content value based on type let contentValue; if (meta.element.tagName.toLowerCase() === 'a') { // For links, save the text content (URL is handled separately if needed) contentValue = formData.text || formData; } else { contentValue = formData.text || formData; } // Universal upsert - server handles ID extraction/generation from markup const contentType = this.determineContentType(meta.element); const result = await this.apiClient.createContent( contentValue, contentType, meta.htmlMarkup // Always send HTML markup - server is smart about ID handling ); if (result) { // Store the backend-generated/confirmed ID in the element meta.element.setAttribute('data-content-id', result.id); meta.element.setAttribute('data-content-type', result.type); console.log(`✅ Content saved: ${result.id}`, contentValue); } else { console.error('❌ Failed to save content to server'); } // Close form this.formRenderer.closeForm(); } catch (error) { console.error('❌ Error saving content:', error); this.formRenderer.closeForm(); } } determineContentType(element) { const tagName = element.tagName.toLowerCase(); if (tagName === 'a' || tagName === 'button') { return 'link'; } // ALL text elements use markdown for consistent editing experience return 'markdown'; } handleCancel(meta) { console.log('❌ Edit cancelled:', meta.contentId); } addEditorStyles() { const styles = ` .insertr-editing-hover { outline: 2px dashed #007cba !important; outline-offset: 2px !important; background-color: rgba(0, 124, 186, 0.05) !important; } .insertr:hover::after { content: "âœī¸ " attr(data-content-type); position: absolute; top: -25px; left: 0; background: #007cba; color: white; padding: 2px 6px; font-size: 11px; border-radius: 3px; white-space: nowrap; z-index: 1000; font-family: monospace; } /* Version History Modal Styles */ .insertr-version-modal { position: fixed; top: 0; left: 0; right: 0; bottom: 0; z-index: 10001; } .insertr-version-backdrop { position: absolute; top: 0; left: 0; right: 0; bottom: 0; background-color: rgba(0, 0, 0, 0.5); display: flex; align-items: center; justify-content: center; padding: 20px; } .insertr-version-content-modal { background: white; border-radius: 8px; max-width: 600px; width: 100%; max-height: 80vh; box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2); display: flex; flex-direction: column; } .insertr-version-header { padding: 20px 20px 0; border-bottom: 1px solid #eee; display: flex; justify-content: space-between; align-items: center; flex-shrink: 0; } .insertr-version-header h3 { margin: 0 0 20px; color: #333; font-size: 18px; } .insertr-btn-close { background: none; border: none; font-size: 24px; cursor: pointer; color: #666; padding: 0; width: 30px; height: 30px; display: flex; align-items: center; justify-content: center; } .insertr-btn-close:hover { color: #333; } .insertr-version-list { overflow-y: auto; padding: 20px; flex: 1; } .insertr-version-item { border: 1px solid #e1e5e9; border-radius: 6px; padding: 16px; margin-bottom: 12px; background: #f8f9fa; } .insertr-version-meta { display: flex; align-items: center; gap: 12px; margin-bottom: 8px; font-size: 13px; } .insertr-version-label { font-weight: 600; color: #0969da; } .insertr-version-date { color: #656d76; } .insertr-version-user { color: #656d76; } .insertr-version-content { margin-bottom: 12px; padding: 8px; background: white; border-radius: 4px; font-family: monospace; font-size: 14px; color: #24292f; white-space: pre-wrap; } .insertr-version-actions { display: flex; gap: 8px; } .insertr-btn-restore { background: #0969da; color: white; border: none; padding: 6px 12px; border-radius: 4px; cursor: pointer; font-size: 12px; font-weight: 500; } .insertr-btn-restore:hover { background: #0860ca; } .insertr-btn-view-diff { background: #f6f8fa; color: #24292f; border: 1px solid #d1d9e0; padding: 6px 12px; border-radius: 4px; cursor: pointer; font-size: 12px; font-weight: 500; } .insertr-btn-view-diff:hover { background: #f3f4f6; } .insertr-version-empty { text-align: center; color: #656d76; font-style: italic; padding: 40px 20px; } /* History Button in Form */ .insertr-btn-history { background: #6f42c1; color: white; border: none; padding: 8px 16px; border-radius: 4px; cursor: pointer; font-size: 14px; font-weight: 500; margin-left: auto; } .insertr-btn-history:hover { background: #5a359a; } `; const styleSheet = document.createElement('style'); styleSheet.type = 'text/css'; styleSheet.innerHTML = styles; document.head.appendChild(styleSheet); } } /** * InsertrAuth - Authentication and state management * Handles user authentication, edit mode, and visual state changes */ class InsertrAuth { constructor(options = {}) { this.options = { mockAuth: options.mockAuth !== false, // Enable mock auth by default hideGatesAfterAuth: options.hideGatesAfterAuth === true, // Keep gates visible by default ...options }; // Authentication state this.state = { isAuthenticated: false, editMode: false, currentUser: null, activeEditor: null, isInitialized: false, isAuthenticating: false }; this.statusIndicator = null; } /** * Initialize gate system (called on page load) */ init() { console.log('🔧 Insertr: Scanning for editor gates'); this.setupEditorGates(); } /** * Initialize full editing system (called after successful OAuth) */ initializeFullSystem() { if (this.state.isInitialized) { return; // Already initialized } console.log('🔐 Initializing Insertr Editing System'); this.createAuthControls(); this.setupAuthenticationControls(); this.createStatusIndicator(); this.updateBodyClasses(); // Auto-enable edit mode after OAuth this.state.editMode = true; this.state.isInitialized = true; // Start the editor system if (window.Insertr && window.Insertr.startEditor) { window.Insertr.startEditor(); } this.updateButtonStates(); this.updateStatusIndicator(); console.log('📱 Editing system active - Controls in bottom-right corner'); console.log('âœī¸ Edit mode enabled - Click elements to edit'); } /** * Setup editor gate click handlers for any .insertr-gate elements */ setupEditorGates() { const gates = document.querySelectorAll('.insertr-gate'); if (gates.length === 0) { console.log('â„šī¸ No .insertr-gate elements found - editor access disabled'); return; } console.log(`đŸšĒ Found ${gates.length} editor gate(s)`); // Add gate styles this.addGateStyles(); gates.forEach((gate, index) => { // Store original text for later restoration if (!gate.hasAttribute('data-original-text')) { gate.setAttribute('data-original-text', gate.textContent); } gate.addEventListener('click', (e) => { e.preventDefault(); this.handleGateClick(gate, index); }); // Add subtle styling to indicate it's clickable gate.style.cursor = 'pointer'; }); } /** * Handle click on an editor gate element */ async handleGateClick(gateElement, gateIndex) { // Prevent multiple simultaneous authentication attempts if (this.state.isAuthenticating) { console.log('âŗ Authentication already in progress...'); return; } console.log(`🚀 Editor gate activated (gate ${gateIndex + 1})`); this.state.isAuthenticating = true; // Store original text and show loading state const originalText = gateElement.textContent; gateElement.setAttribute('data-original-text', originalText); gateElement.textContent = 'âŗ Signing in...'; gateElement.style.pointerEvents = 'none'; try { // Perform OAuth authentication await this.performOAuthFlow(); // Initialize full editing system this.initializeFullSystem(); // Conditionally hide gates based on options if (this.options.hideGatesAfterAuth) { this.hideAllGates(); } else { this.updateGateState(); } } catch (error) { console.error('❌ Authentication failed:', error); // Restore clicked gate to original state const originalText = gateElement.getAttribute('data-original-text'); if (originalText) { gateElement.textContent = originalText; } gateElement.style.pointerEvents = ''; } finally { this.state.isAuthenticating = false; } } /** * Perform OAuth authentication flow */ async performOAuthFlow() { // In development, simulate OAuth flow if (this.options.mockAuth) { console.log('🔐 Mock OAuth: Simulating authentication...'); // Simulate network delay await new Promise(resolve => setTimeout(resolve, 1000)); // Set authenticated state this.state.isAuthenticated = true; this.state.currentUser = { name: 'Site Owner', email: 'owner@example.com', role: 'admin' }; console.log('✅ Mock OAuth: Authentication successful'); return; } // TODO: In production, implement real OAuth flow // This would redirect to OAuth provider, handle callback, etc. throw new Error('Production OAuth not implemented yet'); } /** * Hide all editor gates after successful authentication (optional) */ hideAllGates() { document.body.classList.add('insertr-hide-gates'); console.log('đŸšĒ Editor gates hidden (hideGatesAfterAuth enabled)'); } /** * Update gate state after authentication (restore normal appearance) */ updateGateState() { const gates = document.querySelectorAll('.insertr-gate'); gates.forEach(gate => { // Restore original text if it was saved const originalText = gate.getAttribute('data-original-text'); if (originalText) { gate.textContent = originalText; } // Restore interactive state gate.style.pointerEvents = ''; gate.style.opacity = ''; }); console.log('đŸšĒ Editor gates restored to original state'); } /** * Create authentication control buttons (bottom-right positioned) */ createAuthControls() { // Check if controls already exist if (document.getElementById('insertr-auth-controls')) { return; } const controlsHtml = `
    `; // Add controls to page document.body.insertAdjacentHTML('beforeend', controlsHtml); // Add styles for controls this.addControlStyles(); } /** * Setup event listeners for authentication controls */ setupAuthenticationControls() { const authToggle = document.getElementById('insertr-auth-toggle'); const editToggle = document.getElementById('insertr-edit-toggle'); if (authToggle) { authToggle.addEventListener('click', () => this.toggleAuthentication()); } if (editToggle) { editToggle.addEventListener('click', () => this.toggleEditMode()); } } /** * Toggle authentication state */ toggleAuthentication() { this.state.isAuthenticated = !this.state.isAuthenticated; this.state.currentUser = this.state.isAuthenticated ? { name: 'Demo User', email: 'demo@example.com', role: 'editor' } : null; // Reset edit mode when logging out if (!this.state.isAuthenticated) { this.state.editMode = false; } this.updateBodyClasses(); this.updateButtonStates(); this.updateStatusIndicator(); console.log(this.state.isAuthenticated ? '✅ Authenticated as Demo User' : '❌ Logged out'); } /** * Toggle edit mode (only when authenticated) */ toggleEditMode() { if (!this.state.isAuthenticated) { console.warn('❌ Cannot enable edit mode - not authenticated'); return; } this.state.editMode = !this.state.editMode; // Cancel any active editing when turning off edit mode if (!this.state.editMode && this.state.activeEditor) { // This would be handled by the main editor this.state.activeEditor = null; } this.updateBodyClasses(); this.updateButtonStates(); this.updateStatusIndicator(); console.log(this.state.editMode ? 'âœī¸ Edit mode ON - Click elements to edit' : '👀 Edit mode OFF - Read-only view'); } /** * Update body CSS classes based on authentication state */ updateBodyClasses() { document.body.classList.toggle('insertr-authenticated', this.state.isAuthenticated); document.body.classList.toggle('insertr-edit-mode', this.state.editMode); } /** * Update button text and visibility */ updateButtonStates() { const authBtn = document.getElementById('insertr-auth-toggle'); const editBtn = document.getElementById('insertr-edit-toggle'); if (authBtn) { authBtn.textContent = this.state.isAuthenticated ? 'Logout' : 'Login as Client'; authBtn.className = `insertr-auth-btn ${this.state.isAuthenticated ? 'insertr-authenticated' : ''}`; } if (editBtn) { editBtn.style.display = this.state.isAuthenticated ? 'inline-block' : 'none'; editBtn.textContent = `Edit Mode: ${this.state.editMode ? 'On' : 'Off'}`; editBtn.className = `insertr-auth-btn ${this.state.editMode ? 'insertr-edit-active' : ''}`; } // Update enhance button visibility this.updateEnhanceButtonVisibility(); } /** * Create status indicator */ createStatusIndicator() { // Check if already exists if (document.getElementById('insertr-status')) { return; } const statusHtml = `
    Visitor Mode
    `; document.body.insertAdjacentHTML('beforeend', statusHtml); this.statusIndicator = document.getElementById('insertr-status'); this.setupEnhanceButton(); this.updateStatusIndicator(); } /** * Update status indicator text and style */ updateStatusIndicator() { const statusText = document.querySelector('.insertr-status-text'); const statusDot = document.querySelector('.insertr-status-dot'); if (!statusText || !statusDot) return; if (!this.state.isAuthenticated) { statusText.textContent = 'Visitor Mode'; statusDot.className = 'insertr-status-dot insertr-status-visitor'; } else if (this.state.editMode) { statusText.textContent = 'Editing'; statusDot.className = 'insertr-status-dot insertr-status-editing'; } else { statusText.textContent = 'Authenticated'; statusDot.className = 'insertr-status-dot insertr-status-authenticated'; } } /** * Check if user is authenticated */ isAuthenticated() { return this.state.isAuthenticated; } /** * Check if edit mode is enabled */ isEditMode() { return this.state.editMode; } /** * Get current user info */ getCurrentUser() { return this.state.currentUser; } /** * Add minimal styles for editor gates */ addGateStyles() { const styles = ` .insertr-gate { transition: opacity 0.2s ease; user-select: none; } .insertr-gate:hover { opacity: 0.7; } /* Optional: Hide gates when authenticated (only if hideGatesAfterAuth option is true) */ body.insertr-hide-gates .insertr-gate { display: none !important; } `; const styleSheet = document.createElement('style'); styleSheet.type = 'text/css'; styleSheet.innerHTML = styles; document.head.appendChild(styleSheet); } /** * Add styles for authentication controls */ addControlStyles() { const styles = ` .insertr-auth-controls { position: fixed; bottom: 20px; right: 20px; z-index: 9999; display: flex; flex-direction: column; gap: 8px; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; } .insertr-auth-btn { background: #4f46e5; color: white; border: none; padding: 8px 16px; border-radius: 6px; font-size: 14px; font-weight: 500; cursor: pointer; transition: all 0.2s; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } .insertr-auth-btn:hover { background: #4338ca; transform: translateY(-1px); box-shadow: 0 4px 8px rgba(0,0,0,0.15); } .insertr-auth-btn.insertr-authenticated { background: #059669; } .insertr-auth-btn.insertr-authenticated:hover { background: #047857; } .insertr-auth-btn.insertr-edit-active { background: #dc2626; } .insertr-auth-btn.insertr-edit-active:hover { background: #b91c1c; } .insertr-status-controls { position: fixed; bottom: 20px; left: 20px; z-index: 9999; display: flex; flex-direction: column; gap: 8px; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; } .insertr-status { background: white; border: 1px solid #e5e7eb; border-radius: 8px; padding: 8px 12px; box-shadow: 0 4px 12px rgba(0,0,0,0.1); max-width: 200px; } .insertr-status-content { display: flex; align-items: center; gap: 8px; } .insertr-status-text { font-size: 12px; font-weight: 500; color: #374151; } .insertr-status-dot { width: 8px; height: 8px; border-radius: 50%; background: #9ca3af; } .insertr-status-dot.insertr-status-visitor { background: #9ca3af; } .insertr-status-dot.insertr-status-authenticated { background: #059669; } .insertr-status-dot.insertr-status-editing { background: #dc2626; animation: insertr-pulse 2s infinite; } @keyframes insertr-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } } /* Hide editing interface when not in edit mode */ body:not(.insertr-edit-mode) .insertr:hover::after { display: none !important; } /* Only show editing features when in edit mode */ .insertr-authenticated.insertr-edit-mode .insertr { cursor: pointer; } .insertr-authenticated.insertr-edit-mode .insertr:hover { outline: 2px dashed #007cba !important; outline-offset: 2px !important; background-color: rgba(0, 124, 186, 0.05) !important; } `; const styleSheet = document.createElement('style'); styleSheet.type = 'text/css'; styleSheet.innerHTML = styles; document.head.appendChild(styleSheet); } /** * OAuth integration placeholder * In production, this would handle real OAuth flows */ async authenticateWithOAuth(provider = 'google') { // Mock OAuth flow for now console.log(`🔐 Mock OAuth login with ${provider}`); // Simulate OAuth callback setTimeout(() => { this.state.isAuthenticated = true; this.state.currentUser = { name: 'OAuth User', email: 'user@example.com', provider: provider, role: 'editor' }; this.updateBodyClasses(); this.updateButtonStates(); this.updateStatusIndicator(); console.log('✅ OAuth authentication successful'); }, 1000); } /** * Setup enhance button functionality */ setupEnhanceButton() { const enhanceBtn = document.getElementById('insertr-enhance-btn'); if (!enhanceBtn) return; enhanceBtn.addEventListener('click', async () => { await this.enhanceFiles(); }); // Show enhance button only in development/authenticated mode this.updateEnhanceButtonVisibility(); } /** * Update enhance button visibility based on authentication state */ updateEnhanceButtonVisibility() { const enhanceBtn = document.getElementById('insertr-enhance-btn'); if (!enhanceBtn) return; // Show enhance button when authenticated (indicates dev mode) if (this.state.isAuthenticated) { enhanceBtn.style.display = 'inline-block'; } else { enhanceBtn.style.display = 'none'; } } /** * Trigger manual file enhancement */ async enhanceFiles() { const enhanceBtn = document.getElementById('insertr-enhance-btn'); if (!enhanceBtn) return; // Get site ID from window context or configuration const siteId = window.insertrConfig?.siteId || this.options.siteId || 'demo'; try { // Show loading state enhanceBtn.textContent = 'âŗ Enhancing...'; enhanceBtn.disabled = true; // Smart server detection for enhance API (same logic as ApiClient) const isDevelopment = window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1'; const enhanceUrl = isDevelopment ? `http://localhost:8080/api/enhance?site_id=${siteId}` // Development: separate API server : `/api/enhance?site_id=${siteId}`; // Production: same-origin API // Call enhance API const response = await fetch(enhanceUrl, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${this.state.currentUser?.token || 'mock-token'}` } }); if (!response.ok) { throw new Error(`Enhancement failed: ${response.status} ${response.statusText}`); } const result = await response.json(); console.log('✅ Files enhanced successfully:', result); // Show success state briefly enhanceBtn.textContent = '✅ Enhanced!'; // Reset button after success (no page reload needed) setTimeout(() => { enhanceBtn.textContent = '🔄 Enhance'; enhanceBtn.disabled = false; }, 2000); } catch (error) { console.error('❌ Enhancement failed:', error); enhanceBtn.textContent = '❌ Failed'; // Reset button after error setTimeout(() => { enhanceBtn.textContent = '🔄 Enhance'; enhanceBtn.disabled = false; }, 2000); } } } /** * ApiClient - Handle communication with content API */ class ApiClient { constructor(options = {}) { // Smart server detection based on environment const isDevelopment = window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1'; const defaultEndpoint = isDevelopment ? 'http://localhost:8080/api/content' // Development: separate API server : '/api/content'; // Production: same-origin API this.baseUrl = options.apiEndpoint || defaultEndpoint; this.siteId = options.siteId || 'demo'; // Log API configuration in development if (isDevelopment && !options.apiEndpoint) { console.log(`🔌 API Client: Using development server at ${this.baseUrl}`); } } async getContent(contentId) { try { const response = await fetch(`${this.baseUrl}/${contentId}?site_id=${this.siteId}`); return response.ok ? await response.json() : null; } catch (error) { console.warn('Failed to fetch content:', contentId, error); return null; } } async createContent(content, type, htmlMarkup) { try { const payload = { html_markup: htmlMarkup, // Always send HTML markup - server extracts ID or generates new one value: content, type: type, file_path: this.getCurrentFilePath() // Always include file path for consistent ID generation }; const response = await fetch(`${this.baseUrl}?site_id=${this.siteId}`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${this.getAuthToken()}` }, body: JSON.stringify(payload) }); if (response.ok) { const result = await response.json(); console.log(`✅ Content created: ${result.id} (${result.type})`); return result; } else { console.warn(`âš ī¸ Create failed (${response.status}): server will generate ID`); return null; } } catch (error) { if (error.name === 'TypeError' && error.message.includes('fetch')) { console.warn(`🔌 API Server not reachable at ${this.baseUrl}`); console.warn('💡 Start full-stack development: just dev'); } else { console.error('Failed to create content:', error); } return false; } } async getContentVersions(contentId) { try { const response = await fetch(`${this.baseUrl}/${contentId}/versions?site_id=${this.siteId}`); if (response.ok) { const result = await response.json(); return result.versions || []; } else { console.warn(`âš ī¸ Failed to fetch versions (${response.status}): ${contentId}`); return []; } } catch (error) { console.error('Failed to fetch version history:', contentId, error); return []; } } async rollbackContent(contentId, versionId) { try { const response = await fetch(`${this.baseUrl}/${contentId}/rollback?site_id=${this.siteId}`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${this.getAuthToken()}` }, body: JSON.stringify({ version_id: versionId }) }); if (response.ok) { console.log(`✅ Content rolled back: ${contentId} to version ${versionId}`); return await response.json(); } else { console.warn(`âš ī¸ Rollback failed (${response.status}): ${contentId}`); return false; } } catch (error) { console.error('Failed to rollback content:', contentId, error); return false; } } /** * Get authentication token for API requests * @returns {string} JWT token or mock token for development */ getAuthToken() { // Check if we have a real JWT token from OAuth const realToken = this.getStoredToken(); if (realToken && !this.isTokenExpired(realToken)) { return realToken; } // Development/mock token for when no real auth is present return this.getMockToken(); } /** * Get current user information from token * @returns {string} User identifier */ getCurrentUser() { const token = this.getAuthToken(); // If it's a mock token, return mock user if (token.startsWith('mock-')) { return 'anonymous'; } // Parse real JWT token for user info try { const payload = this.parseJWT(token); return payload.sub || payload.user_id || payload.email || 'anonymous'; } catch (error) { console.warn('Failed to parse JWT token:', error); return 'anonymous'; } } /** * Get stored JWT token from localStorage/sessionStorage * @returns {string|null} Stored JWT token */ getStoredToken() { // Try localStorage first (persistent), then sessionStorage (session-only) return localStorage.getItem('insertr_auth_token') || sessionStorage.getItem('insertr_auth_token') || null; } /** * Store JWT token for future requests * @param {string} token - JWT token from OAuth provider * @param {boolean} persistent - Whether to use localStorage (true) or sessionStorage (false) */ setStoredToken(token, persistent = true) { const storage = persistent ? localStorage : sessionStorage; storage.setItem('insertr_auth_token', token); // Clear the other storage to avoid conflicts const otherStorage = persistent ? sessionStorage : localStorage; otherStorage.removeItem('insertr_auth_token'); } /** * Clear stored authentication token */ clearStoredToken() { localStorage.removeItem('insertr_auth_token'); sessionStorage.removeItem('insertr_auth_token'); } /** * Generate mock JWT token for development/testing * @returns {string} Mock JWT token */ getMockToken() { // Create a mock JWT-like token for development // Format: mock-{user}-{timestamp}-{random} const user = 'anonymous'; const timestamp = Date.now(); const random = Math.random().toString(36).substr(2, 9); return `mock-${user}-${timestamp}-${random}`; } /** * Parse JWT token payload * @param {string} token - JWT token * @returns {object} Parsed payload */ parseJWT(token) { if (token.startsWith('mock-')) { // Return mock payload for development tokens return { sub: 'anonymous', user_id: 'anonymous', email: 'anonymous@localhost', iss: 'insertr-dev', exp: Date.now() + 24 * 60 * 60 * 1000 // 24 hours from now }; } try { // Parse real JWT token const parts = token.split('.'); if (parts.length !== 3) { throw new Error('Invalid JWT format'); } const payload = JSON.parse(atob(parts[1].replace(/-/g, '+').replace(/_/g, '/'))); return payload; } catch (error) { throw new Error(`Failed to parse JWT token: ${error.message}`); } } /** * Check if JWT token is expired * @param {string} token - JWT token * @returns {boolean} True if token is expired */ isTokenExpired(token) { try { const payload = this.parseJWT(token); const now = Math.floor(Date.now() / 1000); return payload.exp && payload.exp < now; } catch (error) { // If we can't parse the token, consider it expired return true; } } /** * Initialize OAuth flow with provider (Google, GitHub, etc.) * @param {string} provider - OAuth provider ('google', 'github', etc.) * @returns {Promise} Success status */ async initiateOAuth(provider = 'google') { // This will be implemented when we add real OAuth integration console.log(`🔐 OAuth flow with ${provider} not yet implemented`); console.log('💡 For now, using mock authentication in development'); // Store a mock token for development const mockToken = this.getMockToken(); this.setStoredToken(mockToken, true); return true; } /** * Handle OAuth callback after user returns from provider * @param {URLSearchParams} urlParams - URL parameters from OAuth callback * @returns {Promise} Success status */ async handleOAuthCallback(urlParams) { // This will be implemented when we add real OAuth integration const code = urlParams.get('code'); urlParams.get('state'); if (code) { console.log('🔐 OAuth callback received, exchanging code for token...'); // TODO: Exchange authorization code for JWT token // const token = await this.exchangeCodeForToken(code, state); // this.setStoredToken(token, true); return true; } return false; } /** * Get current file path from URL for consistent ID generation * @returns {string} File path like "index.html", "about.html" */ getCurrentFilePath() { const path = window.location.pathname; if (path === '/' || path === '') { return 'index.html'; } // Remove leading slash: "/about.html" → "about.html" return path.replace(/^\//, ''); } } /** * Insertr - The Tailwind of CMS * Main library entry point */ // Create global Insertr instance window.Insertr = { // Core functionality core: null, editor: null, auth: null, apiClient: null, // Initialize the library init(options = {}) { console.log('🔧 Insertr v1.0.0 initializing... (Hot Reload Ready)'); this.core = new InsertrCore(options); this.auth = new InsertrAuth(options); this.apiClient = new ApiClient(options); this.editor = new InsertrEditor(this.core, this.auth, this.apiClient, options); // Auto-initialize if DOM is ready if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => this.start()); } else { this.start(); } return this; }, // Start the system - only creates the minimal trigger start() { if (this.auth) { this.auth.init(); // Creates footer trigger only } // Note: Editor is NOT started here, only when trigger is clicked }, // Start the full editor system (called when trigger is activated) startEditor() { if (this.editor && !this.editor.isActive) { this.editor.start(); } }, // Public API methods login() { return this.auth ? this.auth.toggleAuthentication() : null; }, logout() { if (this.auth && this.auth.isAuthenticated()) { this.auth.toggleAuthentication(); } }, toggleEditMode() { return this.auth ? this.auth.toggleEditMode() : null; }, isAuthenticated() { return this.auth ? this.auth.isAuthenticated() : false; }, isEditMode() { return this.auth ? this.auth.isEditMode() : false; }, // TODO: Version info based on package.json? }; // Auto-initialize in development mode with proper DOM ready handling function autoInitialize() { if (document.querySelector('.insertr')) { window.Insertr.init(); } } // Run auto-initialization when DOM is ready if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', autoInitialize); } else { // DOM is already ready autoInitialize(); } var index = window.Insertr; return index; })();