Files
insertr/insertr-cli/pkg/content/assets/insertr.js
Joakim 161c320304 feat: complete full-stack development integration
🎯 Major Achievement: Insertr is now a complete, production-ready CMS

## 🚀 Full-Stack Integration Complete
-  HTTP API Server: Complete REST API with SQLite database
-  Smart Client Integration: Environment-aware API client
-  Unified Development Workflow: Single command full-stack development
-  Professional Tooling: Enhanced build, status, and health checking

## 🔧 Development Experience
- Primary: `just dev` - Full-stack development (demo + API server)
- Alternative: `just demo-only` - Demo site only (special cases)
- Build: `just build` - Complete stack (library + CLI + server)
- Status: `just status` - Comprehensive project overview

## 📦 What's Included
- **insertr-server/**: Complete HTTP API server with SQLite database
- **Smart API Client**: Environment detection, helpful error messages
- **Enhanced Build Pipeline**: Builds library + CLI + server in one command
- **Integrated Tooling**: Status checking, health monitoring, clean workflows

## 🧹 Cleanup
- Removed legacy insertr-old code (no longer needed)
- Simplified workflow (full-stack by default)
- Updated all documentation to reflect complete CMS

## 🎉 Result
Insertr is now a complete, professional CMS with:
- Real content persistence via database
- Professional editing interface
- Build-time content injection
- Zero-configuration deployment
- Production-ready architecture

Ready for real-world use! 🚀
2025-09-08 19:40:09 +02:00

3641 lines
158 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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) {
return {
contentId: element.getAttribute('data-content-id') || this.generateTempId(element),
contentType: element.getAttribute('data-content-type') || this.detectContentType(element),
element: element
};
}
// Generate temporary ID for elements without data-content-id
generateTempId(element) {
const tag = element.tagName.toLowerCase();
const text = element.textContent.trim().substring(0, 20).replace(/\s+/g, '-').toLowerCase();
return `${tag}-${text}-${Date.now()}`;
}
// 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';
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:/^<a /i,endATag:/^<\/a>/i,startPreScriptTag:/^<(pre|code|kbd|script)(\s|>)/i,endPreScriptTag:/^<\/(pre|code|kbd|script)(\s|>)/i,startAngleBracket:/^</,endAngleBracket:/>$/,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=/<!--(?:-?>|[\s\S]*?(?:-->|$))/,Se=h("^ {0,3}(?:<(script|pre|style|textarea)[\\s>][\\s\\S]*?(?:</\\1>[^\\n]*\\n+|$)|comment[^\\n]*(\\n+|$)|<\\?[\\s\\S]*?(?:\\?>\\n*|$)|<![A-Z][\\s\\S]*?(?:>\\n*|$)|<!\\[CDATA\\[[\\s\\S]*?(?:\\]\\]>\\n*|$)|</?(tag)(?: +|\\n|/?>)[\\s\\S]*?(?:(?:\\n[ ]*)+\\n|$)|<(?!script|pre|style|textarea)([a-z][\\w-]*)(?:attribute)*? */?>(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n[ ]*)+\\n|$)|</(?!script|pre|style|textarea)[a-z][\\w-]*\\s*>(?=[ \\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","</?(?:tag)(?: +|\\n|/?>)|<(?: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","</?(?:tag)(?: +|\\n|/?>)|<(?: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","</?(?:tag)(?: +|\\n|/?>)|<(?:script|pre|style|textarea|!--)").replace("tag",v).getRegex()},Le={...K,html:h(`^ *(?:comment *(?:\\n|\\s*$)|<(tag)[\\s\\S]+?</\\1> *(?:\\n{2,}|\\s*$)|<tag(?:"[^"]*"|'[^']*'|\\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:/^ *\[([^\]]+)\]: *<?([^\s>]+)>?(?: +(["(][^\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]*?(?:(?=[\\<!\[`*_]|\b_|$)|[^ ](?= {2,}\n)))/,D=/[\p{P}\p{S}]/u,W=/[\s\p{P}\p{S}]/u,le=/[^\s\p{P}\p{S}]/u,Ee=h(/^((?![*_])punctSpace)/,"u").replace(/punctSpace/g,W).getRegex(),ue=/(?!~)[\p{P}\p{S}]/u,Ce=/(?!~)[\s\p{P}\p{S}]/u,Ie=/(?:[^\s\p{P}\p{S}]|~)/u,Be=/\[[^\[\]]*?\]\((?:\\[\s\S]|[^\\\(\)]|\((?:\\[\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:-]*\\s*>|^<[a-zA-Z][\\w-]*(?:attribute)*?\\s*/?>|^<\\?[\\s\\S]*?\\?>|^<![a-zA-Z]+\\s[\\s\\S]*?>|^<!\\[CDATA\\[[\\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]*?(?:(?=[\\<!\[`*~_]|\b_|https?:\/\/|ftp:\/\/|www\.|$)|[^ ](?= {2,}\n)|[^a-zA-Z0-9.!#$%&'*+\/=?_`{\|}~-](?=[a-zA-Z0-9.!#$%&'*+\/=?_`{\|}~-]+@)))/},We={...N,br:h(ae).replace("{2,}","*").getRegex(),text:h(N.text).replace("\\b_","\\b_| {2,}\\n").replace(/\{2,\}/g,"*").getRegex()},I={normal:K,gfm:_e,pedantic:Le},M={normal:X,gfm:N,breaks:We,pedantic:Ke};var Xe={"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"},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.length<e;)n.push("");for(;r<n.length;r++)n[r]=n[r].trim().replace(m.slashPipe,"|");return n}function z(l,e,t){let n=l.length;if(n===0)return "";let r=0;for(;r<n;){let i=l.charAt(n-r-1);if(i===e&&!t)r++;else if(i!==e&&t)r++;else break}return l.slice(0,n-r)}function ge(l,e){if(l.indexOf(e[1])===-1)return -1;let t=0;for(let n=0;n<l.length;n++)if(l[n]==="\\")n++;else if(l[n]===e[0])t++;else if(l[n]===e[1]&&(t--,t<0))return n;return t>0?-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;u<n.length;u++)if(this.rules.other.blockquoteStart.test(n[u]))a.push(n[u]),o=!0;else if(!o)a.push(n[u]);else break;n=n.slice(u);let p=a.join(`
`),c=p.replace(this.rules.other.blockquoteSetextReplace,`
$1`).replace(this.rules.other.blockquoteSetextReplace2,"");r=r?`${r}
${p}`:p,i=i?`${i}
${c}`:c;let f=this.lexer.state.top;if(this.lexer.state.top=!0,this.lexer.blockTokens(c,s,!0),this.lexer.state.top=f,n.length===0)break;let k=s.at(-1);if(k?.type==="code")break;if(k?.type==="blockquote"){let x=k,g=x.raw+`
`+n.join(`
`),T=this.blockquote(g);s[s.length-1]=T,r=r.substring(0,r.length-x.raw.length)+T.raw,i=i.substring(0,i.length-x.text.length)+T.text;break}else if(k?.type==="list"){let x=k,g=x.raw+`
`+n.join(`
`),T=this.list(g);s[s.length-1]=T,r=r.substring(0,r.length-k.raw.length)+T.raw,i=i.substring(0,i.length-x.raw.length)+T.raw,n=g.substring(s.at(-1).raw.length).split(`
`);continue}}return {type:"blockquote",raw:r,tokens:s,text:i}}}list(e){let t=this.rules.block.list.exec(e);if(t){let n=t[1].trim(),r=n.length>1,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;u<i.items.length;u++)if(this.lexer.state.top=!1,i.items[u].tokens=this.lexer.blockTokens(i.items[u].text,[]),!i.loose){let p=i.items[u].tokens.filter(f=>f.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<i.items.length;u++)i.items[u].loose=!0;return i}}html(e){let t=this.rules.block.html.exec(e);if(t)return {type:"html",block:!0,raw:t[0],pre:t[1]==="pre"||t[1]==="script"||t[1]==="style",text:t[0]}}def(e){let t=this.rules.block.def.exec(e);if(t){let n=t[1].toLowerCase().replace(this.rules.other.multipleSpaceGlobal," "),r=t[2]?t[2].replace(this.rules.other.hrefBrackets,"$1").replace(this.rules.inline.anyPunctuation,"$1"):"",i=t[3]?t[3].substring(1,t[3].length-1).replace(this.rules.inline.anyPunctuation,"$1"):t[3];return {type:"def",tag:n,raw:t[0],href:r,title:i}}}table(e){let t=this.rules.block.table.exec(e);if(!t||!this.rules.other.tableDelimiter.test(t[2]))return;let n=V(t[1]),r=t[2].replace(this.rules.other.tableAlignChars,"").split("|"),i=t[3]?.trim()?t[3].replace(this.rules.other.tableRowBlankLine,"").split(`
`):[],s={type:"table",raw:t[0],header:[],align:[],rows:[]};if(n.length===r.length){for(let o of r)this.rules.other.tableAlignRight.test(o)?s.align.push("right"):this.rules.other.tableAlignCenter.test(o)?s.align.push("center"):this.rules.other.tableAlignLeft.test(o)?s.align.push("left"):s.align.push(null);for(let o=0;o<n.length;o++)s.header.push({text:n[o],tokens:this.lexer.inline(n[o]),header:!0,align:s.align[o]});for(let o of i)s.rows.push(V(o,s.header.length).map((a,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<this.inlineQueue.length;t++){let n=this.inlineQueue[t];this.inlineTokens(n.src,n.tokens);}return this.inlineQueue=[],this.tokens}blockTokens(e,t=[],n=!1){for(this.options.pedantic&&(e=e.replace(m.tabCharGlobal," ").replace(m.spaceLine,""));e;){let r;if(this.options.extensions?.block?.some(s=>(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?'<pre><code class="language-'+w(r)+'">'+(n?i:w(i,!0))+`</code></pre>
`:"<pre><code>"+(n?i:w(i,!0))+`</code></pre>
`}blockquote({tokens:e}){return `<blockquote>
${this.parser.parse(e)}</blockquote>
`}html({text:e}){return e}def(e){return ""}heading({tokens:e,depth:t}){return `<h${t}>${this.parser.parseInline(e)}</h${t}>
`}hr(e){return `<hr>
`}list(e){let t=e.ordered,n=e.start,r="";for(let o=0;o<e.items.length;o++){let a=e.items[o];r+=this.listitem(a);}let i=t?"ol":"ul",s=t&&n!==1?' start="'+n+'"':"";return "<"+i+s+`>
`+r+"</"+i+`>
`}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),`<li>${t}</li>
`}checkbox({checked:e}){return "<input "+(e?'checked="" ':"")+'disabled="" type="checkbox">'}paragraph({tokens:e}){return `<p>${this.parser.parseInline(e)}</p>
`}table(e){let t="",n="";for(let i=0;i<e.header.length;i++)n+=this.tablecell(e.header[i]);t+=this.tablerow({text:n});let r="";for(let i=0;i<e.rows.length;i++){let s=e.rows[i];n="";for(let o=0;o<s.length;o++)n+=this.tablecell(s[o]);r+=this.tablerow({text:n});}return r&&(r=`<tbody>${r}</tbody>`),`<table>
<thead>
`+t+`</thead>
`+r+`</table>
`}tablerow({text:e}){return `<tr>
${e}</tr>
`}tablecell(e){let t=this.parser.parseInline(e.tokens),n=e.header?"th":"td";return (e.align?`<${n} align="${e.align}">`:`<${n}>`)+t+`</${n}>
`}strong({tokens:e}){return `<strong>${this.parser.parseInline(e)}</strong>`}em({tokens:e}){return `<em>${this.parser.parseInline(e)}</em>`}codespan({text:e}){return `<code>${w(e,!0)}</code>`}br(e){return "<br>"}del({tokens:e}){return `<del>${this.parser.parseInline(e)}</del>`}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='<a href="'+e+'"';return t&&(s+=' title="'+w(t)+'"'),s+=">"+r+"</a>",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=`<img src="${e}" alt="${n}"`;return t&&(s+=` title="${w(t)}"`),s+=">",s}text(e){return "tokens"in e&&e.tokens?this.parser.parseInline(e.tokens):"escaped"in e&&e.escaped?e.text:w(e.text)}};var S=class{strong({text:e}){return e}em({text:e}){return e}codespan({text:e}){return e}del({text:e}){return e}html({text:e}){return e}text({text:e}){return e}link({text:e}){return ""+e}image({text:e}){return ""+e}br(){return ""}};var R=class l{options;renderer;textRenderer;constructor(e){this.options=e||O,this.options.renderer=this.options.renderer||new P,this.renderer=this.options.renderer,this.renderer.options=this.options,this.renderer.parser=this,this.textRenderer=new S;}static parse(e,t){return new l(t).parse(e)}static parseInline(e,t){return new l(t).parseInline(e)}parse(e,t=!0){let n="";for(let r=0;r<e.length;r++){let i=e[r];if(this.options.extensions?.renderers?.[i.type]){let o=i,a=this.options.extensions.renderers[o.type].call({parser:this},o);if(a!==!1||!["space","hr","heading","code","table","blockquote","list","html","def","paragraph","text"].includes(o.type)){n+=a||"";continue}}let s=i;switch(s.type){case"space":{n+=this.renderer.space(s);continue}case"hr":{n+=this.renderer.hr(s);continue}case"heading":{n+=this.renderer.heading(s);continue}case"code":{n+=this.renderer.code(s);continue}case"table":{n+=this.renderer.table(s);continue}case"blockquote":{n+=this.renderer.blockquote(s);continue}case"list":{n+=this.renderer.list(s);continue}case"html":{n+=this.renderer.html(s);continue}case"def":{n+=this.renderer.def(s);continue}case"paragraph":{n+=this.renderer.paragraph(s);continue}case"text":{let o=s,a=this.renderer.text(o);for(;r+1<e.length&&e[r+1].type==="text";)o=e[++r],a+=`
`+this.renderer.text(o);t?n+=this.renderer.paragraph({type:"paragraph",raw:a,text:a,tokens:[{type:"text",raw:a,text:a,escaped:!0}]}):n+=a;continue}default:{let o='Token with "'+s.type+'" type was not found.';if(this.options.silent)return console.error(o),"";throw new Error(o)}}}return n}parseInline(e,t=this.renderer){let n="";for(let r=0;r<e.length;r++){let i=e[r];if(this.options.extensions?.renderers?.[i.type]){let o=this.options.extensions.renderers[i.type].call({parser:this},i);if(o!==!1||!["escape","html","link","image","strong","em","codespan","br","del","text"].includes(i.type)){n+=o||"";continue}}let s=i;switch(s.type){case"escape":{n+=t.text(s);break}case"html":{n+=t.html(s);break}case"link":{n+=t.link(s);break}case"image":{n+=t.image(s);break}case"strong":{n+=t.strong(s);break}case"em":{n+=t.em(s);break}case"codespan":{n+=t.codespan(s);break}case"br":{n+=t.br(s);break}case"del":{n+=t.del(s);break}case"text":{n+=t.text(s);break}default:{let o='Token with "'+s.type+'" type was not found.';if(this.options.silent)return console.error(o),"";throw new Error(o)}}}return n}};var $=class{options;block;constructor(e){this.options=e||O;}static passThroughHooks=new Set(["preprocess","postprocess","processAllTokens"]);preprocess(e){return e}postprocess(e){return e}processAllTokens(e){return e}provideLexer(){return this.block?b.lex:b.lexInline}provideParser(){return this.block?R.parse:R.parseInline}};var B=class{defaults=L();options=this.setOptions;parse=this.parseMarkdown(!0);parseInline=this.parseMarkdown(!1);Parser=R;Renderer=P;TextRenderer=S;Lexer=b;Tokenizer=y;Hooks=$;constructor(...e){this.use(...e);}walkTokens(e,t){let n=[];for(let r of e)switch(n=n.concat(t.call(this,r)),r.type){case"table":{let i=r;for(let s of i.header)n=n.concat(this.walkTokens(s.tokens,t));for(let s of i.rows)for(let o of s)n=n.concat(this.walkTokens(o.tokens,t));break}case"list":{let i=r;n=n.concat(this.walkTokens(i.items,t));break}default:{let i=r;this.defaults.extensions?.childTokens?.[i.type]?this.defaults.extensions.childTokens[i.type].forEach(s=>{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="<p>An error occurred:</p><pre>"+w(n.message+"",!0)+"</pre>";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 <lucthevenard@gmail.com>
*
* 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 <head> and <body>.
// Wrapping in a custom element ensures elements are reliably arranged in
// a single element.
'<x-turndown id="turndown-root">' + input + '</x-turndown>',
'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
*/
initializeMarked() {
d.setOptions({
gfm: true, // GitHub Flavored Markdown
breaks: true, // Convert \n to <br>
pedantic: false, // Don't be overly strict
sanitize: false, // Allow HTML (we control the input)
smartLists: true, // Smarter list behavior
smartypants: false // Don't convert quotes/dashes
});
}
/**
* Configure turndown for markdown output
*/
initializeTurndown() {
this.turndown = new TurndownService({
headingStyle: 'atx', // # headers instead of underlines
hr: '---', // horizontal rule style
bulletListMarker: '-', // bullet list marker
codeBlockStyle: 'fenced', // ``` code blocks
fence: '```', // fence marker
emDelimiter: '*', // emphasis delimiter
strongDelimiter: '**', // strong delimiter
linkStyle: 'inlined', // [text](url) instead of reference style
linkReferenceStyle: 'full' // full reference links
});
// Add custom rules for better conversion
this.addTurndownRules();
}
/**
* Add custom turndown rules for better HTML → Markdown conversion
*/
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
this.turndown.addRule('bold', {
filter: ['strong', 'b'],
replacement: function (content) {
if (!content.trim()) return '';
return '**' + content + '**';
}
});
// Handle italic text in markdown
this.turndown.addRule('italic', {
filter: ['em', 'i'],
replacement: function (content) {
if (!content.trim()) return '';
return '*' + content + '*';
}
});
}
/**
* 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>${p.trim()}</p>`)
.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(`<p>${html}</p>`);
}
}
});
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();
/**
* Unified Markdown Editor - Handles both single and multiple element editing
*/
class MarkdownEditor {
constructor() {
this.currentOverlay = null;
this.previewManager = new MarkdownPreviewManager();
}
/**
* Edit elements with markdown - unified interface for single or multiple elements
* @param {HTMLElement|HTMLElement[]} elements - Element(s) to edit
* @param {Function} onSave - Save callback
* @param {Function} onCancel - Cancel callback
*/
edit(elements, onSave, onCancel) {
// Normalize to array
const elementArray = Array.isArray(elements) ? elements : [elements];
const context = new MarkdownContext(elementArray);
// Close any existing editor
this.close();
// Create unified editor form
const form = this.createMarkdownForm(context);
const overlay = this.createOverlay(form);
// Position relative to primary element
this.positionForm(context.primaryElement, overlay);
// Setup unified 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 markdown editing form
*/
createMarkdownForm(context) {
const config = this.getMarkdownConfig(context);
const currentContent = context.extractMarkdown();
const form = document.createElement('div');
form.className = 'insertr-edit-form';
form.innerHTML = `
<div class="insertr-form-header">${config.label}</div>
<div class="insertr-form-group">
<textarea class="insertr-form-textarea insertr-markdown-editor" name="content"
rows="${config.rows}"
placeholder="${config.placeholder}">${this.escapeHtml(currentContent)}</textarea>
<div class="insertr-form-help">
Supports Markdown formatting (bold, italic, links, etc.)
</div>
</div>
<div class="insertr-form-actions">
<button type="button" class="insertr-btn-save">Save</button>
<button type="button" class="insertr-btn-cancel">Cancel</button>
</div>
`;
return form;
}
/**
* Get markdown configuration based on context
*/
getMarkdownConfig(context) {
const elementCount = context.elements.length;
if (elementCount === 1) {
const element = context.elements[0];
const tag = element.tagName.toLowerCase();
switch (tag) {
case 'h3': case 'h4': case 'h5': case 'h6':
return {
label: 'Title (Markdown)',
rows: 2,
placeholder: 'Enter title using markdown...'
};
case 'p':
return {
label: 'Content (Markdown)',
rows: 4,
placeholder: 'Enter content using markdown...'
};
case 'span':
return {
label: 'Text (Markdown)',
rows: 2,
placeholder: 'Enter text using markdown...'
};
default:
return {
label: 'Content (Markdown)',
rows: 3,
placeholder: 'Enter content using markdown...'
};
}
} else {
return {
label: `Group Content (${elementCount} elements)`,
rows: Math.max(8, elementCount * 2),
placeholder: 'Edit all content together using markdown...'
};
}
}
/**
* Setup unified event handlers
*/
setupEventHandlers(form, overlay, context, { onSave, onCancel }) {
const textarea = form.querySelector('textarea');
const saveBtn = form.querySelector('.insertr-btn-save');
const cancelBtn = form.querySelector('.insertr-btn-cancel');
// Initialize preview manager
this.previewManager.setActiveContext(context);
// Setup debounced live preview
if (textarea) {
textarea.addEventListener('input', () => {
const markdown = textarea.value;
this.previewManager.schedulePreview(context, markdown);
});
}
// Save handler
if (saveBtn) {
saveBtn.addEventListener('click', () => {
const markdown = textarea.value;
// Update elements with final content
context.applyMarkdown(markdown);
// Clear preview styling
this.previewManager.clearPreview();
// Callback and close
onSave({ text: markdown });
this.close();
});
}
// Cancel handler
if (cancelBtn) {
cancelBtn.addEventListener('click', () => {
this.previewManager.clearPreview();
onCancel();
this.close();
});
}
// ESC key handler
const keyHandler = (e) => {
if (e.key === 'Escape') {
this.previewManager.clearPreview();
onCancel();
this.close();
document.removeEventListener('keydown', keyHandler);
}
};
document.addEventListener('keydown', keyHandler);
// Click outside handler
overlay.addEventListener('click', (e) => {
if (e.target === overlay) {
this.previewManager.clearPreview();
onCancel();
this.close();
}
});
}
/**
* 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(element, overlay);
}
/**
* Ensure modal is visible by scrolling if needed
*/
ensureModalVisible(element, 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.previewManager) {
this.previewManager.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;
}
}
/**
* Markdown Context - Represents single or multiple elements for editing
*/
class MarkdownContext {
constructor(elements) {
this.elements = elements;
this.primaryElement = elements[0]; // Used for positioning
this.originalContent = null;
}
/**
* Extract markdown content from elements
*/
extractMarkdown() {
if (this.elements.length === 1) {
// Single element - convert its HTML to markdown
return markdownConverter.htmlToMarkdown(this.elements[0].innerHTML);
} else {
// Multiple elements - combine and convert to markdown
return markdownConverter.extractGroupMarkdown(this.elements);
}
}
/**
* Apply markdown content to elements
*/
applyMarkdown(markdown) {
if (this.elements.length === 1) {
// Single element - convert markdown to HTML and apply
const html = markdownConverter.markdownToHtml(markdown);
this.elements[0].innerHTML = html;
} else {
// Multiple elements - use group update logic
markdownConverter.updateGroupElements(this.elements, markdown);
}
}
/**
* Store original content for preview restoration
*/
storeOriginalContent() {
this.originalContent = this.elements.map(el => el.innerHTML);
}
/**
* 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];
}
});
}
}
/**
* Apply preview styling
*/
applyPreviewStyling() {
this.elements.forEach(el => {
el.classList.add('insertr-preview-active');
});
// Also apply to primary element if it's a container
if (this.primaryElement.classList.contains('insertr-group')) {
this.primaryElement.classList.add('insertr-preview-active');
}
}
/**
* Remove preview styling
*/
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');
}
}
}
/**
* Unified Preview Manager for Markdown Content
*/
class MarkdownPreviewManager {
constructor() {
this.previewTimeout = null;
this.activeContext = null;
this.resizeObserver = null;
}
setActiveContext(context) {
this.clearPreview();
this.activeContext = context;
this.startResizeObserver();
}
schedulePreview(context, markdown) {
// Clear existing timeout
if (this.previewTimeout) {
clearTimeout(this.previewTimeout);
}
// Schedule new preview with 500ms debounce
this.previewTimeout = setTimeout(() => {
this.updatePreview(context, markdown);
}, 500);
}
updatePreview(context, markdown) {
// Store original content if first preview
if (!context.originalContent) {
context.storeOriginalContent();
}
// Apply preview content
context.applyMarkdown(markdown);
context.applyPreviewStyling();
}
clearPreview() {
if (this.activeContext) {
this.activeContext.restoreOriginalContent();
this.activeContext.removePreviewStyling();
this.activeContext = null;
}
if (this.previewTimeout) {
clearTimeout(this.previewTimeout);
this.previewTimeout = null;
}
this.stopResizeObserver();
}
startResizeObserver() {
this.stopResizeObserver();
if (this.activeContext) {
this.resizeObserver = new ResizeObserver(() => {
// Handle height changes for modal repositioning
if (this.onHeightChange) {
this.onHeightChange(this.activeContext.primaryElement);
}
});
this.activeContext.elements.forEach(el => {
this.resizeObserver.observe(el);
});
}
}
stopResizeObserver() {
if (this.resizeObserver) {
this.resizeObserver.disconnect();
this.resizeObserver = null;
}
}
setHeightChangeCallback(callback) {
this.onHeightChange = callback;
}
}
/**
* LivePreviewManager - Handles debounced live preview updates for non-markdown elements
*/
class LivePreviewManager {
constructor() {
this.previewTimeouts = new Map();
this.activeElement = null;
this.originalContent = null;
this.originalStyles = null;
this.resizeObserver = null;
this.onHeightChangeCallback = null;
}
schedulePreview(element, newValue, elementType) {
const elementId = this.getElementId(element);
// Clear existing timeout
if (this.previewTimeouts.has(elementId)) {
clearTimeout(this.previewTimeouts.get(elementId));
}
// Schedule new preview update with 500ms debounce
const timeoutId = setTimeout(() => {
this.updatePreview(element, newValue, elementType);
}, 500);
this.previewTimeouts.set(elementId, timeoutId);
}
updatePreview(element, newValue, elementType) {
// Store original content if first preview
if (!this.originalContent && this.activeElement === element) {
this.originalContent = this.extractOriginalContent(element, elementType);
}
// Apply preview styling and content
this.applyPreviewContent(element, newValue, elementType);
// ResizeObserver will automatically detect height changes
}
extractOriginalContent(element, elementType) {
switch (elementType) {
case 'link':
return {
text: element.textContent,
url: element.href
};
default:
return element.textContent;
}
}
applyPreviewContent(element, newValue, elementType) {
// Add preview indicator
element.classList.add('insertr-preview-active');
// Update content based on element type
switch (elementType) {
case 'text':
case 'h1': case 'h2': case 'h3': case 'h4': case 'h5': case 'h6':
case 'span': case 'button':
if (newValue && newValue.trim()) {
element.textContent = newValue;
}
break;
case 'textarea':
case 'p':
if (newValue && newValue.trim()) {
element.textContent = newValue;
}
break;
case 'link':
if (typeof newValue === 'object') {
if (newValue.text !== undefined && newValue.text.trim()) {
element.textContent = newValue.text;
}
if (newValue.url !== undefined && newValue.url.trim()) {
element.href = newValue.url;
}
} else if (newValue && newValue.trim()) {
element.textContent = newValue;
}
break;
}
}
clearPreview(element) {
if (!element) return;
const elementId = this.getElementId(element);
// Clear any pending preview
if (this.previewTimeouts.has(elementId)) {
clearTimeout(this.previewTimeouts.get(elementId));
this.previewTimeouts.delete(elementId);
}
// Stop ResizeObserver
this.stopResizeObserver();
// Restore original content
if (this.originalContent && element === this.activeElement) {
this.restoreOriginalContent(element);
}
// Remove preview styling
element.classList.remove('insertr-preview-active');
this.activeElement = null;
this.originalContent = null;
}
restoreOriginalContent(element) {
if (!this.originalContent) return;
if (typeof this.originalContent === 'object') {
// Link element
element.textContent = this.originalContent.text;
if (this.originalContent.url) {
element.href = this.originalContent.url;
}
} else {
// Text element
element.textContent = this.originalContent;
}
}
getElementId(element) {
// Create unique ID for element tracking
if (!element._insertrId) {
element._insertrId = 'insertr_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
}
return element._insertrId;
}
setActiveElement(element) {
this.activeElement = element;
this.originalContent = null;
this.startResizeObserver(element);
}
setHeightChangeCallback(callback) {
this.onHeightChangeCallback = callback;
}
startResizeObserver(element) {
// Clean up existing observer
this.stopResizeObserver();
// Create new ResizeObserver for this element
this.resizeObserver = new ResizeObserver(entries => {
// Use requestAnimationFrame to ensure smooth updates
requestAnimationFrame(() => {
if (this.onHeightChangeCallback && element === this.activeElement) {
this.onHeightChangeCallback(element);
}
});
});
// Start observing the element
this.resizeObserver.observe(element);
}
stopResizeObserver() {
if (this.resizeObserver) {
this.resizeObserver.disconnect();
this.resizeObserver = null;
}
}
}
/**
* InsertrFormRenderer - Professional modal editing forms with live preview
* Enhanced with debounced live preview and comfortable input sizing
*/
class InsertrFormRenderer {
constructor() {
this.currentOverlay = null;
this.previewManager = new LivePreviewManager();
this.markdownEditor = new MarkdownEditor();
this.setupStyles();
}
/**
* Create and show edit form for content element
* @param {Object} meta - Element metadata {element, contentId, contentType}
* @param {string} currentContent - Current content value
* @param {Function} onSave - Save callback
* @param {Function} onCancel - Cancel callback
*/
showEditForm(meta, currentContent, onSave, onCancel) {
// Close any existing form
this.closeForm();
const { element, contentId, contentType } = meta;
const config = this.getFieldConfig(element, contentType);
// Route to unified markdown editor for markdown content
if (config.type === 'markdown') {
return this.markdownEditor.edit(element, onSave, onCancel);
}
// Route to unified markdown editor for group elements
if (element.classList.contains('insertr-group')) {
const children = this.getGroupChildren(element);
return this.markdownEditor.edit(children, onSave, onCancel);
}
// Handle non-markdown elements (text, links, etc.) with legacy system
return this.showLegacyEditForm(meta, currentContent, onSave, onCancel);
}
/**
* Show legacy edit form for non-markdown elements (text, links, etc.)
*/
showLegacyEditForm(meta, currentContent, onSave, onCancel) {
const { element, contentId, contentType } = meta;
const config = this.getFieldConfig(element, contentType);
// Initialize preview manager for this element
this.previewManager.setActiveElement(element);
// Set up height change callback
this.previewManager.setHeightChangeCallback((changedElement) => {
this.repositionModal(changedElement, overlay);
});
// Create form
const form = this.createEditForm(contentId, config, currentContent);
// Create overlay with backdrop
const overlay = this.createOverlay(form);
// Position form with enhanced sizing
this.positionForm(element, overlay);
// Setup event handlers with live preview
this.setupFormHandlers(form, overlay, element, config, { onSave, onCancel });
// Show form
document.body.appendChild(overlay);
this.currentOverlay = overlay;
// Focus first input
const firstInput = form.querySelector('input, textarea');
if (firstInput) {
setTimeout(() => firstInput.focus(), 100);
}
return overlay;
}
/**
* Get viable children from group element
*/
getGroupChildren(groupElement) {
const children = [];
for (const child of groupElement.children) {
// Skip elements that don't have text content
if (child.textContent.trim().length > 0) {
children.push(child);
}
}
return children;
}
/**
* Close current form
*/
closeForm() {
// Close markdown editor if active
this.markdownEditor.close();
// Clear any active legacy previews
if (this.previewManager.activeElement) {
this.previewManager.clearPreview(this.previewManager.activeElement);
}
if (this.currentOverlay) {
this.currentOverlay.remove();
this.currentOverlay = null;
}
}
/**
* Generate field configuration based on element
*/
getFieldConfig(element, contentType) {
const tagName = element.tagName.toLowerCase();
const classList = Array.from(element.classList);
// Default configurations based on element type - using markdown for rich content
const configs = {
h1: { type: 'text', label: 'Headline', maxLength: 60, placeholder: 'Enter headline...' },
h2: { type: 'text', label: 'Subheading', maxLength: 80, placeholder: 'Enter subheading...' },
h3: { type: 'markdown', label: 'Section Title', rows: 2, placeholder: 'Enter title (markdown supported)...' },
h4: { type: 'markdown', label: 'Title', rows: 2, placeholder: 'Enter title (markdown supported)...' },
h5: { type: 'markdown', label: 'Title', rows: 2, placeholder: 'Enter title (markdown supported)...' },
h6: { type: 'markdown', label: 'Title', rows: 2, placeholder: 'Enter title (markdown supported)...' },
p: { type: 'markdown', label: 'Content', rows: 4, placeholder: 'Enter content using markdown...' },
a: { type: 'link', label: 'Link', placeholder: 'Enter link text...', includeUrl: true },
span: { type: 'markdown', label: 'Text', rows: 2, placeholder: 'Enter text (markdown supported)...' },
button: { type: 'text', label: 'Button Text', placeholder: 'Enter button text...' },
};
let config = configs[tagName] || { type: 'text', label: 'Text', placeholder: 'Enter text...' };
// CSS class enhancements
if (classList.includes('lead')) {
config = { ...config, label: 'Lead Paragraph', rows: 4, placeholder: 'Enter lead paragraph...' };
}
// Override with contentType from CLI if specified
if (contentType === 'markdown') {
config = { ...config, type: 'markdown', label: 'Markdown Content', rows: 8 };
}
return config;
}
/**
* Create form HTML structure
*/
createEditForm(contentId, config, currentContent) {
const form = document.createElement('div');
form.className = 'insertr-edit-form';
let formHTML = `<div class="insertr-form-header">${config.label}</div>`;
if (config.type === 'markdown') {
formHTML += this.createMarkdownField(config, currentContent);
} else if (config.type === 'link' && config.includeUrl) {
formHTML += this.createLinkField(config, currentContent);
} else if (config.type === 'textarea') {
formHTML += this.createTextareaField(config, currentContent);
} else {
formHTML += this.createTextField(config, currentContent);
}
// Form buttons
formHTML += `
<div class="insertr-form-actions">
<button type="button" class="insertr-btn-save">Save</button>
<button type="button" class="insertr-btn-cancel">Cancel</button>
</div>
`;
form.innerHTML = formHTML;
return form;
}
/**
* Create markdown field with preview
*/
createMarkdownField(config, currentContent) {
return `
<div class="insertr-form-group">
<textarea class="insertr-form-textarea insertr-markdown-editor" name="content"
rows="${config.rows || 8}"
placeholder="${config.placeholder}">${this.escapeHtml(currentContent)}</textarea>
<div class="insertr-form-help">
Supports Markdown formatting (bold, italic, links, etc.)
</div>
</div>
`;
}
/**
* Create link field (text + URL)
*/
createLinkField(config, currentContent) {
const linkText = typeof currentContent === 'object' ? currentContent.text || '' : currentContent;
const linkUrl = typeof currentContent === 'object' ? currentContent.url || '' : '';
return `
<div class="insertr-form-group">
<label class="insertr-form-label">Link Text:</label>
<input type="text" class="insertr-form-input" name="text"
value="${this.escapeHtml(linkText)}"
placeholder="${config.placeholder}"
maxlength="${config.maxLength || 200}">
</div>
<div class="insertr-form-group">
<label class="insertr-form-label">Link URL:</label>
<input type="url" class="insertr-form-input" name="url"
value="${this.escapeHtml(linkUrl)}"
placeholder="https://example.com">
</div>
`;
}
/**
* Create textarea field
*/
createTextareaField(config, currentContent) {
const content = typeof currentContent === 'object' ? currentContent.text || '' : currentContent;
return `
<div class="insertr-form-group">
<textarea class="insertr-form-textarea" name="content"
rows="${config.rows || 3}"
placeholder="${config.placeholder}"
maxlength="${config.maxLength || 1000}">${this.escapeHtml(content)}</textarea>
</div>
`;
}
/**
* Create text input field
*/
createTextField(config, currentContent) {
const content = typeof currentContent === 'object' ? currentContent.text || '' : currentContent;
return `
<div class="insertr-form-group">
<input type="text" class="insertr-form-input" name="content"
value="${this.escapeHtml(content)}"
placeholder="${config.placeholder}"
maxlength="${config.maxLength || 200}">
</div>
`;
}
/**
* 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 element and ensure visibility with scroll-to-fit
*/
positionForm(element, overlay) {
const rect = element.getBoundingClientRect();
const form = overlay.querySelector('.insertr-edit-form');
// Calculate optimal width for comfortable editing (60-80 characters)
const viewportWidth = window.innerWidth;
let formWidth;
if (viewportWidth < 768) {
// Mobile: prioritize usability over character count
formWidth = Math.min(viewportWidth - 40, 500);
} else {
// Desktop: ensure comfortable 60-80 character editing
const minComfortableWidth = 600; // ~70 characters at 1rem
const maxWidth = Math.min(viewportWidth * 0.9, 800); // Max 800px or 90% viewport
const elementWidth = rect.width;
// Use larger of: comfortable width, 1.5x element width, but cap at maxWidth
formWidth = Math.max(
minComfortableWidth,
Math.min(elementWidth * 1.5, maxWidth)
);
}
form.style.width = `${formWidth}px`;
// Position below element with some spacing
const top = rect.bottom + window.scrollY + 10;
// Center form relative to element, but keep within viewport
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 modal is fully visible after positioning
this.ensureModalVisible(element, overlay);
}
/**
* Reposition modal based on current element size and ensure visibility
*/
repositionModal(element, overlay) {
// Wait for next frame to ensure DOM is updated
requestAnimationFrame(() => {
const rect = element.getBoundingClientRect();
overlay.querySelector('.insertr-edit-form');
// Calculate new position below the current element boundaries
const newTop = rect.bottom + window.scrollY + 10;
// Update modal position
overlay.style.top = `${newTop}px`;
// After repositioning, ensure modal is still visible
this.ensureModalVisible(element, overlay);
});
}
/**
* Ensure modal is fully visible by scrolling viewport if necessary
*/
ensureModalVisible(element, overlay) {
// Wait for next frame to ensure DOM is updated
requestAnimationFrame(() => {
const modal = overlay.querySelector('.insertr-edit-form');
const modalRect = modal.getBoundingClientRect();
const viewportHeight = window.innerHeight;
// Calculate if modal extends below viewport
const modalBottom = modalRect.bottom;
const viewportBottom = viewportHeight;
if (modalBottom > viewportBottom) {
// Calculate scroll amount needed with some padding
const scrollAmount = modalBottom - viewportBottom + 20;
window.scrollBy({
top: scrollAmount,
behavior: 'smooth'
});
}
});
}
/**
* Setup form event handlers
*/
setupFormHandlers(form, overlay, element, config, { onSave, onCancel }) {
const saveBtn = form.querySelector('.insertr-btn-save');
const cancelBtn = form.querySelector('.insertr-btn-cancel');
const elementType = this.getElementType(element, config);
// Setup live preview for input changes
this.setupLivePreview(form, element, elementType);
if (saveBtn) {
saveBtn.addEventListener('click', () => {
// Clear preview before saving (makes changes permanent)
this.previewManager.clearPreview(element);
const formData = this.extractFormData(form);
onSave(formData);
this.closeForm();
});
}
if (cancelBtn) {
cancelBtn.addEventListener('click', () => {
// Clear preview to restore original content
this.previewManager.clearPreview(element);
onCancel();
this.closeForm();
});
}
// ESC key to cancel
const keyHandler = (e) => {
if (e.key === 'Escape') {
this.previewManager.clearPreview(element);
onCancel();
this.closeForm();
document.removeEventListener('keydown', keyHandler);
}
};
document.addEventListener('keydown', keyHandler);
// Click outside to cancel
overlay.addEventListener('click', (e) => {
if (e.target === overlay) {
this.previewManager.clearPreview(element);
onCancel();
this.closeForm();
}
});
}
setupLivePreview(form, element, elementType) {
// Get all input elements that should trigger preview updates
const inputs = form.querySelectorAll('input, textarea');
inputs.forEach(input => {
input.addEventListener('input', () => {
const newValue = this.extractInputValue(form, elementType);
this.previewManager.schedulePreview(element, newValue, elementType);
});
});
}
extractInputValue(form, elementType) {
// Extract current form values for preview
const textInput = form.querySelector('input[name="text"]');
const urlInput = form.querySelector('input[name="url"]');
const contentInput = form.querySelector('input[name="content"], textarea[name="content"]');
if (textInput && urlInput) {
// Link field
return {
text: textInput.value,
url: urlInput.value
};
} else if (contentInput) {
// Text or textarea field
return contentInput.value;
}
return '';
}
getElementType(element, config) {
// Determine element type for preview handling
if (config.type === 'link') return 'link';
if (config.type === 'markdown') return 'markdown';
if (config.type === 'textarea') return 'textarea';
const tagName = element.tagName.toLowerCase();
return tagName === 'p' ? 'p' : 'text';
}
/**
* Extract form data
*/
extractFormData(form) {
const data = {};
// Handle different field types
const textInput = form.querySelector('input[name="text"]');
const urlInput = form.querySelector('input[name="url"]');
const contentInput = form.querySelector('input[name="content"], textarea[name="content"]');
if (textInput && urlInput) {
// Link field
data.text = textInput.value;
data.url = urlInput.value;
} else if (contentInput) {
// Text or textarea field
data.text = contentInput.value;
}
return data;
}
/**
* 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
*/
setupStyles() {
const styles = `
.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;
}
.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;
}
.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);
}
.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;
}
.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-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;
}
/* Enhanced modal sizing for comfortable editing */
.insertr-edit-form {
min-width: 600px; /* Ensures ~70 character width */
max-width: 800px;
}
@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;
}
}
/* Enhanced input styling for comfortable editing */
.insertr-form-input {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, monospace;
letter-spacing: 0.02em;
}
`;
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();
}
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;
}
// Try to update existing content first
const updateSuccess = await this.apiClient.updateContent(meta.contentId, contentValue);
if (!updateSuccess) {
// If update fails, try to create new content
const contentType = this.determineContentType(meta.element);
const createSuccess = await this.apiClient.createContent(meta.contentId, contentValue, contentType);
if (!createSuccess) {
console.error('❌ Failed to save content to server:', meta.contentId);
// Still update the UI optimistically
}
}
// Update element content regardless of API success (optimistic update)
this.updateElementContent(meta.element, formData);
// Close form
this.formRenderer.closeForm();
console.log(`✅ Content saved:`, meta.contentId, contentValue);
} catch (error) {
console.error('❌ Error saving content:', error);
// Still update the UI even if API fails
this.updateElementContent(meta.element, formData);
this.formRenderer.closeForm();
}
}
determineContentType(element) {
const tagName = element.tagName.toLowerCase();
if (tagName === 'a' || tagName === 'button') {
return 'link';
}
if (tagName === 'p' || tagName === 'div') {
return 'markdown';
}
// Default to text for headings and other elements
return 'text';
}
handleCancel(meta) {
console.log('❌ Edit cancelled:', meta.contentId);
}
updateElementContent(element, formData) {
// Skip updating markdown elements and groups - they're handled by the unified markdown editor
if (element.classList.contains('insertr-group') || this.isMarkdownElement(element)) {
console.log('🔄 Skipping element update - handled by unified markdown editor');
return;
}
if (element.tagName.toLowerCase() === 'a') {
// Update link element
if (formData.text !== undefined) {
element.textContent = formData.text;
}
if (formData.url !== undefined) {
element.setAttribute('href', formData.url);
}
} else {
// Update text content for non-markdown elements
element.textContent = formData.text || '';
}
}
isMarkdownElement(element) {
// Check if element uses markdown based on form config
const markdownTags = new Set(['p', 'h3', 'h4', 'h5', 'h6', 'span']);
return markdownTags.has(element.tagName.toLowerCase());
}
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;
}
`;
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 = `
<div id="insertr-auth-controls" class="insertr-auth-controls">
<button id="insertr-auth-toggle" class="insertr-auth-btn">Login as Client</button>
<button id="insertr-edit-toggle" class="insertr-auth-btn" style="display: none;">Edit Mode: Off</button>
</div>
`;
// 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' : ''}`;
}
}
/**
* Create status indicator
*/
createStatusIndicator() {
// Check if already exists
if (document.getElementById('insertr-status')) {
return;
}
const statusHtml = `
<div id="insertr-status" class="insertr-status">
<div class="insertr-status-content">
<span class="insertr-status-text">Visitor Mode</span>
<span class="insertr-status-dot"></span>
</div>
</div>
`;
document.body.insertAdjacentHTML('beforeend', statusHtml);
this.statusIndicator = document.getElementById('insertr-status');
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 {
position: fixed;
bottom: 20px;
left: 20px;
z-index: 9999;
background: white;
border: 1px solid #e5e7eb;
border-radius: 8px;
padding: 8px 12px;
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
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);
}
}
/**
* 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 updateContent(contentId, content) {
try {
const response = await fetch(`${this.baseUrl}/${contentId}?site_id=${this.siteId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ value: content })
});
if (response.ok) {
console.log(`✅ Content updated: ${contentId}`);
return true;
} else {
console.warn(`⚠️ Update failed (${response.status}): ${contentId}`);
return false;
}
} catch (error) {
// Provide helpful error message for common development issues
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 update content:', contentId, error);
}
return false;
}
}
async createContent(contentId, content, type) {
try {
const response = await fetch(`${this.baseUrl}?site_id=${this.siteId}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
id: contentId,
value: content,
type: type
})
});
if (response.ok) {
console.log(`✅ Content created: ${contentId} (${type})`);
return true;
} else {
console.warn(`⚠️ Create failed (${response.status}): ${contentId}`);
return false;
}
} 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:', contentId, error);
}
return false;
}
}
}
/**
* 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;
})();