- Move all ContentItem, ContentClient, ContentResponse types to engine/types.go as single source of truth - Remove duplicate type definitions from content/types.go - Update all imports across codebase to use engine types - Enhance engine to extract existing data-content-id from HTML markup - Simplify frontend to always send html_markup, let server handle ID extraction/generation - Fix contentId reference errors in frontend error handling - Add getAttribute helper method to engine for ID extraction - Add GetAllContent method to engine.DatabaseClient - Update enhancer to use engine.ContentClient interface - All builds and API endpoints verified working This resolves the 400 Bad Request errors and creates a unified architecture where the server is the single source of truth for all ID generation and content type management.
4123 lines
173 KiB
JavaScript
4123 lines
173 KiB
JavaScript
var Insertr = (function () {
|
||
'use strict';
|
||
|
||
/**
|
||
* InsertrCore - Core functionality for content management
|
||
*/
|
||
class InsertrCore {
|
||
constructor(options = {}) {
|
||
this.options = {
|
||
apiEndpoint: options.apiEndpoint || '/api/content',
|
||
siteId: options.siteId || 'default',
|
||
...options
|
||
};
|
||
}
|
||
|
||
// Find all enhanced elements on the page with container expansion
|
||
findEnhancedElements() {
|
||
const directElements = document.querySelectorAll('.insertr');
|
||
const expandedElements = [];
|
||
|
||
directElements.forEach(element => {
|
||
if (this.isContainer(element) && !element.classList.contains('insertr-group')) {
|
||
// Container element (.insertr) - expand to viable children
|
||
const children = this.findViableChildren(element);
|
||
expandedElements.push(...children);
|
||
} else {
|
||
// Regular element or group (.insertr-group)
|
||
expandedElements.push(element);
|
||
}
|
||
});
|
||
|
||
return expandedElements;
|
||
}
|
||
|
||
// Check if element is a container that should expand to children
|
||
isContainer(element) {
|
||
const containerTags = new Set([
|
||
'div', 'section', 'article', 'header',
|
||
'footer', 'main', 'aside', 'nav'
|
||
]);
|
||
|
||
return containerTags.has(element.tagName.toLowerCase());
|
||
}
|
||
|
||
// Find viable children for editing (elements with only text content)
|
||
findViableChildren(containerElement) {
|
||
const viable = [];
|
||
|
||
for (const child of containerElement.children) {
|
||
// Skip elements that already have .insertr class
|
||
if (child.classList.contains('insertr')) {
|
||
continue;
|
||
}
|
||
|
||
// Skip self-closing elements
|
||
if (this.isSelfClosing(child)) {
|
||
continue;
|
||
}
|
||
|
||
// Check if element has only text content (no nested HTML elements)
|
||
if (this.hasOnlyTextContent(child)) {
|
||
viable.push(child);
|
||
}
|
||
}
|
||
|
||
return viable;
|
||
}
|
||
|
||
// Check if element is viable for editing (allows simple formatting)
|
||
hasOnlyTextContent(element) {
|
||
// Allow elements with simple formatting tags
|
||
const allowedTags = new Set(['strong', 'b', 'em', 'i', 'a', 'span', 'code']);
|
||
|
||
for (const child of element.children) {
|
||
const tagName = child.tagName.toLowerCase();
|
||
|
||
// If child is not an allowed formatting tag, reject
|
||
if (!allowedTags.has(tagName)) {
|
||
return false;
|
||
}
|
||
|
||
// If formatting tag has nested complex elements, reject
|
||
if (child.children.length > 0) {
|
||
// Recursively check nested content isn't too complex
|
||
for (const nestedChild of child.children) {
|
||
const nestedTag = nestedChild.tagName.toLowerCase();
|
||
if (!allowedTags.has(nestedTag)) {
|
||
return false;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// Element has only text and/or simple formatting - this is viable
|
||
return element.textContent.trim().length > 0;
|
||
}
|
||
|
||
// Check if element is self-closing
|
||
isSelfClosing(element) {
|
||
const selfClosingTags = new Set([
|
||
'img', 'input', 'br', 'hr', 'meta', 'link',
|
||
'area', 'base', 'col', 'embed', 'source', 'track', 'wbr'
|
||
]);
|
||
|
||
return selfClosingTags.has(element.tagName.toLowerCase());
|
||
}
|
||
|
||
// Get element metadata
|
||
getElementMetadata(element) {
|
||
const existingId = element.getAttribute('data-content-id');
|
||
|
||
// Ensure element has insertr class for server processing
|
||
if (!element.classList.contains('insertr')) {
|
||
element.classList.add('insertr');
|
||
}
|
||
|
||
// Send HTML markup to server for unified ID generation
|
||
return {
|
||
contentId: existingId, // null if new content, existing ID if updating
|
||
contentType: element.getAttribute('data-content-type') || this.detectContentType(element),
|
||
element: element,
|
||
htmlMarkup: element.outerHTML // Server will generate ID from this
|
||
};
|
||
}
|
||
|
||
// Get current file path from URL for consistent ID generation
|
||
getCurrentFilePath() {
|
||
const path = window.location.pathname;
|
||
if (path === '/' || path === '') {
|
||
return 'index.html';
|
||
}
|
||
// Remove leading slash: "/about.html" → "about.html"
|
||
return path.replace(/^\//, '');
|
||
}
|
||
|
||
// Detect content type for elements without data-content-type
|
||
detectContentType(element) {
|
||
const tag = element.tagName.toLowerCase();
|
||
|
||
if (element.classList.contains('insertr-group')) {
|
||
return 'markdown';
|
||
}
|
||
|
||
switch (tag) {
|
||
case 'h1': case 'h2': case 'h3': case 'h4': case 'h5': case 'h6':
|
||
return 'text';
|
||
case 'p':
|
||
return 'textarea';
|
||
case 'a': case 'button':
|
||
return 'link';
|
||
case 'div': case 'section':
|
||
return 'markdown';
|
||
case 'span':
|
||
return 'markdown'; // Match backend: spans support inline markdown
|
||
default:
|
||
return 'text';
|
||
}
|
||
}
|
||
|
||
// Get all elements with their metadata, including group elements
|
||
getAllElements() {
|
||
const directElements = document.querySelectorAll('.insertr, .insertr-group');
|
||
const processedElements = [];
|
||
|
||
directElements.forEach(element => {
|
||
if (element.classList.contains('insertr-group')) {
|
||
// Group element - treat as single editable unit
|
||
processedElements.push(element);
|
||
} else if (this.isContainer(element)) {
|
||
// Container element - expand to children
|
||
const children = this.findViableChildren(element);
|
||
processedElements.push(...children);
|
||
} else {
|
||
// Regular element
|
||
processedElements.push(element);
|
||
}
|
||
});
|
||
|
||
return Array.from(processedElements).map(el => this.getElementMetadata(el));
|
||
}
|
||
}
|
||
|
||
/**
|
||
* marked v16.2.1 - a markdown parser
|
||
* Copyright (c) 2011-2025, Christopher Jeffrey. (MIT Licensed)
|
||
* https://github.com/markedjs/marked
|
||
*/
|
||
|
||
/**
|
||
* DO NOT EDIT THIS FILE
|
||
* The code in this file is generated from files in ./src/
|
||
*/
|
||
|
||
function L(){return {async:!1,breaks:!1,extensions:null,gfm:!0,hooks:null,pedantic:!1,renderer:null,silent:!1,tokenizer:null,walkTokens:null}}var O=L();function H(l){O=l;}var E={exec:()=>null};function h(l,e=""){let t=typeof l=="string"?l:l.source,n={replace:(r,i)=>{let s=typeof i=="string"?i:i.source;return s=s.replace(m.caret,"$1"),t=t.replace(r,s),n},getRegex:()=>new RegExp(t,e)};return n}var m={codeRemoveIndent:/^(?: {1,4}| {0,3}\t)/gm,outputLinkReplace:/\\([\[\]])/g,indentCodeCompensation:/^(\s+)(?:```)/,beginningSpace:/^\s+/,endingHash:/#$/,startingSpaceChar:/^ /,endingSpaceChar:/ $/,nonSpaceChar:/[^ ]/,newLineCharGlobal:/\n/g,tabCharGlobal:/\t/g,multipleSpaceGlobal:/\s+/g,blankLine:/^[ \t]*$/,doubleBlankLine:/\n[ \t]*\n[ \t]*$/,blockquoteStart:/^ {0,3}>/,blockquoteSetextReplace:/\n {0,3}((?:=+|-+) *)(?=\n|$)/g,blockquoteSetextReplace2:/^ {0,3}>[ \t]?/gm,listReplaceTabs:/^\t+/,listReplaceNesting:/^ {1,4}(?=( {4})*[^ ])/g,listIsTask:/^\[[ xX]\] /,listReplaceTask:/^\[[ xX]\] +/,anyLine:/\n.*\n/,hrefBrackets:/^<(.*)>$/,tableDelimiter:/[:|]/,tableAlignChars:/^\||\| *$/g,tableRowBlankLine:/\n[ \t]*$/,tableAlignRight:/^ *-+: *$/,tableAlignCenter:/^ *:-+: *$/,tableAlignLeft:/^ *:-+ *$/,startATag:/^<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={"&":"&","<":"<",">":">",'"':""","'":"'"},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 - MINIMAL MODE
|
||
* Only supports: **bold**, *italic*, and [links](url)
|
||
* Matches server-side goldmark configuration
|
||
*/
|
||
initializeMarked() {
|
||
d.setOptions({
|
||
gfm: false, // Disable GFM to match server minimal mode
|
||
breaks: true, // Convert \n to <br> (matches server)
|
||
pedantic: false, // Don't be overly strict
|
||
sanitize: false, // Allow HTML (we control the input)
|
||
smartLists: false, // Disable lists (not supported on server)
|
||
smartypants: false // Don't convert quotes/dashes
|
||
});
|
||
|
||
// Override renderers to restrict to minimal feature set
|
||
d.use({
|
||
renderer: {
|
||
// Disable headings - treat as plain text
|
||
heading(text, level) {
|
||
return text;
|
||
},
|
||
// Disable lists - treat as plain text
|
||
list(body, ordered, start) {
|
||
return body.replace(/<\/?li>/g, '');
|
||
},
|
||
listitem(text) {
|
||
return text + '\n';
|
||
},
|
||
// Disable code blocks - treat as plain text
|
||
code(code, language) {
|
||
return code;
|
||
},
|
||
blockquote(quote) {
|
||
return quote; // Disable blockquotes - treat as plain text
|
||
},
|
||
// Disable horizontal rules
|
||
hr() {
|
||
return '';
|
||
},
|
||
// Disable tables
|
||
table(header, body) {
|
||
return header + body;
|
||
},
|
||
tablecell(content, flags) {
|
||
return content;
|
||
},
|
||
tablerow(content) {
|
||
return content;
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Configure turndown for markdown output - MINIMAL MODE
|
||
* Only supports: **bold**, *italic*, and [links](url)
|
||
* Matches server-side goldmark configuration
|
||
*/
|
||
initializeTurndown() {
|
||
this.turndown = new TurndownService({
|
||
// Minimal configuration - only basic formatting
|
||
headingStyle: 'atx', // # headers (but will be disabled)
|
||
hr: '---', // horizontal rule (but will be disabled)
|
||
bulletListMarker: '-', // bullet list (but will be disabled)
|
||
codeBlockStyle: 'fenced', // code blocks (but will be disabled)
|
||
fence: '```', // fence marker (but will be disabled)
|
||
emDelimiter: '*', // *italic* - matches server
|
||
strongDelimiter: '**', // **bold** - matches server
|
||
linkStyle: 'inlined', // [text](url) - matches server
|
||
linkReferenceStyle: 'full' // full reference links
|
||
});
|
||
|
||
// Add custom rules for better conversion
|
||
this.addTurndownRules();
|
||
}
|
||
|
||
/**
|
||
* Add custom turndown rules - MINIMAL MODE
|
||
* Only supports: **bold**, *italic*, and [links](url)
|
||
* Disables all other formatting to match server
|
||
*/
|
||
addTurndownRules() {
|
||
// Handle paragraph spacing properly - ensure double newlines between paragraphs
|
||
this.turndown.addRule('paragraph', {
|
||
filter: 'p',
|
||
replacement: function (content) {
|
||
if (!content.trim()) return '';
|
||
return content.trim() + '\n\n';
|
||
}
|
||
});
|
||
|
||
// Handle bold text in markdown - keep this (supported)
|
||
this.turndown.addRule('bold', {
|
||
filter: ['strong', 'b'],
|
||
replacement: function (content) {
|
||
if (!content.trim()) return '';
|
||
return '**' + content + '**';
|
||
}
|
||
});
|
||
|
||
// Handle italic text in markdown - keep this (supported)
|
||
this.turndown.addRule('italic', {
|
||
filter: ['em', 'i'],
|
||
replacement: function (content) {
|
||
if (!content.trim()) return '';
|
||
return '*' + content + '*';
|
||
}
|
||
});
|
||
|
||
// DISABLE unsupported features - convert to plain text
|
||
this.turndown.addRule('disableHeadings', {
|
||
filter: ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'],
|
||
replacement: function (content) {
|
||
return content; // Just return text content, no # markup
|
||
}
|
||
});
|
||
|
||
this.turndown.addRule('disableLists', {
|
||
filter: ['ul', 'ol', 'li'],
|
||
replacement: function (content) {
|
||
return content; // Just return text content, no list markup
|
||
}
|
||
});
|
||
|
||
this.turndown.addRule('disableCode', {
|
||
filter: ['pre', 'code'],
|
||
replacement: function (content) {
|
||
return content; // Just return text content, no code markup
|
||
}
|
||
});
|
||
|
||
this.turndown.addRule('disableBlockquotes', {
|
||
filter: 'blockquote',
|
||
replacement: function (content) {
|
||
return content; // Just return text content, no > markup
|
||
}
|
||
});
|
||
|
||
this.turndown.addRule('disableHR', {
|
||
filter: 'hr',
|
||
replacement: function () {
|
||
return ''; // Remove horizontal rules entirely
|
||
}
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Convert HTML to Markdown
|
||
* @param {string} html - HTML string to convert
|
||
* @returns {string} - Markdown string
|
||
*/
|
||
htmlToMarkdown(html) {
|
||
if (!html || html.trim() === '') {
|
||
return '';
|
||
}
|
||
|
||
try {
|
||
const markdown = this.turndown.turndown(html);
|
||
// Clean up and normalize newlines for proper paragraph separation
|
||
return markdown
|
||
.replace(/\n{3,}/g, '\n\n') // Replace 3+ newlines with 2
|
||
.replace(/^\n+|\n+$/g, '') // Remove leading/trailing newlines
|
||
.trim(); // Remove other whitespace
|
||
} catch (error) {
|
||
console.warn('HTML to Markdown conversion failed:', error);
|
||
// Fallback: extract text content
|
||
const tempDiv = document.createElement('div');
|
||
tempDiv.innerHTML = html;
|
||
return tempDiv.textContent || tempDiv.innerText || '';
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Convert Markdown to HTML
|
||
* @param {string} markdown - Markdown string to convert
|
||
* @returns {string} - HTML string
|
||
*/
|
||
markdownToHtml(markdown) {
|
||
if (!markdown || markdown.trim() === '') {
|
||
return '';
|
||
}
|
||
|
||
try {
|
||
const html = d(markdown);
|
||
return html;
|
||
} catch (error) {
|
||
console.warn('Markdown to HTML conversion failed:', error);
|
||
// Fallback: convert line breaks to paragraphs
|
||
return markdown
|
||
.split(/\n\s*\n/)
|
||
.filter(p => p.trim())
|
||
.map(p => `<p>${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();
|
||
|
||
/**
|
||
* Previewer - Handles live preview for all content types
|
||
*/
|
||
|
||
class Previewer {
|
||
constructor() {
|
||
this.previewTimeout = null;
|
||
this.activeContext = null;
|
||
this.resizeObserver = null;
|
||
this.onHeightChange = null;
|
||
}
|
||
|
||
/**
|
||
* Set the active editing context for preview
|
||
*/
|
||
setActiveContext(context) {
|
||
this.clearPreview();
|
||
this.activeContext = context;
|
||
this.startResizeObserver();
|
||
}
|
||
|
||
/**
|
||
* Schedule a preview update with debouncing
|
||
*/
|
||
schedulePreview(context, content) {
|
||
// Clear existing timeout
|
||
if (this.previewTimeout) {
|
||
clearTimeout(this.previewTimeout);
|
||
}
|
||
|
||
// Schedule new preview with 500ms debounce
|
||
this.previewTimeout = setTimeout(() => {
|
||
this.updatePreview(context, content);
|
||
}, 500);
|
||
}
|
||
|
||
/**
|
||
* Update preview with new content
|
||
*/
|
||
updatePreview(context, content) {
|
||
// Store original content if first preview
|
||
if (!context.originalContent) {
|
||
context.storeOriginalContent();
|
||
}
|
||
|
||
// Apply preview content to elements
|
||
this.applyPreviewContent(context, content);
|
||
context.applyPreviewStyling();
|
||
}
|
||
|
||
/**
|
||
* Apply preview content to context elements
|
||
*/
|
||
applyPreviewContent(context, content) {
|
||
if (context.elements.length === 1) {
|
||
const element = context.elements[0];
|
||
|
||
// Handle links specially
|
||
if (element.tagName.toLowerCase() === 'a') {
|
||
if (typeof content === 'object') {
|
||
// Update link text (markdown to HTML)
|
||
if (content.text !== undefined) {
|
||
const html = markdownConverter.markdownToHtml(content.text);
|
||
element.innerHTML = html;
|
||
}
|
||
// Update link URL
|
||
if (content.url !== undefined && content.url.trim()) {
|
||
element.href = content.url;
|
||
}
|
||
} else if (content && content.trim()) {
|
||
// Just markdown content for link text
|
||
const html = markdownConverter.markdownToHtml(content);
|
||
element.innerHTML = html;
|
||
}
|
||
return;
|
||
}
|
||
|
||
// Regular single element
|
||
if (content && content.trim()) {
|
||
const html = markdownConverter.markdownToHtml(content);
|
||
element.innerHTML = html;
|
||
}
|
||
} else {
|
||
// Multiple elements - use group update
|
||
if (content && content.trim()) {
|
||
markdownConverter.updateGroupElements(context.elements, content);
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Clear all preview state and restore original content
|
||
*/
|
||
clearPreview() {
|
||
if (this.activeContext) {
|
||
this.activeContext.restoreOriginalContent();
|
||
this.activeContext.removePreviewStyling();
|
||
this.activeContext = null;
|
||
}
|
||
|
||
if (this.previewTimeout) {
|
||
clearTimeout(this.previewTimeout);
|
||
this.previewTimeout = null;
|
||
}
|
||
|
||
this.stopResizeObserver();
|
||
}
|
||
|
||
/**
|
||
* Start observing element size changes for modal repositioning
|
||
*/
|
||
startResizeObserver() {
|
||
this.stopResizeObserver();
|
||
|
||
if (this.activeContext) {
|
||
this.resizeObserver = new ResizeObserver(() => {
|
||
// Handle height changes for modal repositioning
|
||
if (this.onHeightChange) {
|
||
this.onHeightChange(this.activeContext.primaryElement);
|
||
}
|
||
});
|
||
|
||
// Observe all elements in the context
|
||
this.activeContext.elements.forEach(el => {
|
||
this.resizeObserver.observe(el);
|
||
});
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Stop observing element size changes
|
||
*/
|
||
stopResizeObserver() {
|
||
if (this.resizeObserver) {
|
||
this.resizeObserver.disconnect();
|
||
this.resizeObserver = null;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Set callback for height changes (for modal repositioning)
|
||
*/
|
||
setHeightChangeCallback(callback) {
|
||
this.onHeightChange = callback;
|
||
}
|
||
|
||
/**
|
||
* Get unique element ID for tracking
|
||
*/
|
||
getElementId(element) {
|
||
if (!element._insertrId) {
|
||
element._insertrId = 'insertr_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
|
||
}
|
||
return element._insertrId;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Editor - Handles all content types with markdown-first approach
|
||
*/
|
||
|
||
class Editor {
|
||
constructor() {
|
||
this.currentOverlay = null;
|
||
this.previewer = new Previewer();
|
||
}
|
||
|
||
/**
|
||
* Edit any content element with markdown interface
|
||
* @param {Object} meta - Element metadata {element, contentId, contentType}
|
||
* @param {string|Object} currentContent - Current content value
|
||
* @param {Function} onSave - Save callback
|
||
* @param {Function} onCancel - Cancel callback
|
||
*/
|
||
edit(meta, currentContent, onSave, onCancel) {
|
||
const { element } = meta;
|
||
|
||
// Handle both single elements and groups uniformly
|
||
const elements = Array.isArray(element) ? element : [element];
|
||
const context = new EditContext(elements, currentContent);
|
||
|
||
// Close any existing editor
|
||
this.close();
|
||
|
||
// Create editor form
|
||
const form = this.createForm(context, meta);
|
||
const overlay = this.createOverlay(form);
|
||
|
||
// Position relative to primary element
|
||
this.positionForm(context.primaryElement, overlay);
|
||
|
||
// Setup event handlers
|
||
this.setupEventHandlers(form, overlay, context, { onSave, onCancel });
|
||
|
||
// Show editor
|
||
document.body.appendChild(overlay);
|
||
this.currentOverlay = overlay;
|
||
|
||
// Focus textarea
|
||
const textarea = form.querySelector('textarea');
|
||
if (textarea) {
|
||
setTimeout(() => textarea.focus(), 100);
|
||
}
|
||
|
||
return overlay;
|
||
}
|
||
|
||
/**
|
||
* Create editing form for any content type
|
||
*/
|
||
createForm(context, meta) {
|
||
const config = this.getFieldConfig(context);
|
||
const currentContent = context.extractContent();
|
||
|
||
const form = document.createElement('div');
|
||
form.className = 'insertr-edit-form';
|
||
|
||
// Build form HTML
|
||
let formHTML = `<div class="insertr-form-header">${config.label}</div>`;
|
||
|
||
// Markdown textarea (always present)
|
||
formHTML += this.createMarkdownField(config, currentContent);
|
||
|
||
// URL field (for links only)
|
||
if (config.includeUrl) {
|
||
formHTML += this.createUrlField(currentContent);
|
||
}
|
||
|
||
// Form actions
|
||
formHTML += `
|
||
<div class="insertr-form-actions">
|
||
<button type="button" class="insertr-btn-save">Save</button>
|
||
<button type="button" class="insertr-btn-cancel">Cancel</button>
|
||
<button type="button" class="insertr-btn-history" data-content-id="${meta.contentId}">View History</button>
|
||
</div>
|
||
`;
|
||
|
||
form.innerHTML = formHTML;
|
||
return form;
|
||
}
|
||
|
||
/**
|
||
* Get field configuration for any element type (markdown-first)
|
||
*/
|
||
getFieldConfig(context) {
|
||
const elementCount = context.elements.length;
|
||
const primaryElement = context.primaryElement;
|
||
const isLink = primaryElement.tagName.toLowerCase() === 'a';
|
||
|
||
// Multi-element groups
|
||
if (elementCount > 1) {
|
||
return {
|
||
type: 'markdown',
|
||
includeUrl: false,
|
||
label: `Group Content (${elementCount} elements)`,
|
||
rows: Math.max(8, elementCount * 2),
|
||
placeholder: 'Edit all content together using markdown...'
|
||
};
|
||
}
|
||
|
||
// Single elements - all get markdown by default
|
||
const tag = primaryElement.tagName.toLowerCase();
|
||
const baseConfig = {
|
||
type: 'markdown',
|
||
includeUrl: isLink,
|
||
placeholder: 'Enter content using markdown...'
|
||
};
|
||
|
||
// Customize by element type
|
||
switch (tag) {
|
||
case 'h1':
|
||
return { ...baseConfig, label: 'Main Headline', rows: 1, placeholder: 'Enter main headline...' };
|
||
case 'h2':
|
||
return { ...baseConfig, label: 'Subheading', rows: 1, placeholder: 'Enter subheading...' };
|
||
case 'h3': case 'h4': case 'h5': case 'h6':
|
||
return { ...baseConfig, label: 'Heading', rows: 2, placeholder: 'Enter heading (markdown supported)...' };
|
||
case 'p':
|
||
return { ...baseConfig, label: 'Content', rows: 4, placeholder: 'Enter content using markdown...' };
|
||
case 'span':
|
||
return { ...baseConfig, label: 'Text', rows: 2, placeholder: 'Enter text (markdown supported)...' };
|
||
case 'button':
|
||
return { ...baseConfig, label: 'Button Text', rows: 1, placeholder: 'Enter button text...' };
|
||
case 'a':
|
||
return { ...baseConfig, label: 'Link', rows: 2, placeholder: 'Enter link text (markdown supported)...' };
|
||
default:
|
||
return { ...baseConfig, label: 'Content', rows: 3, placeholder: 'Enter content using markdown...' };
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Create markdown textarea field
|
||
*/
|
||
createMarkdownField(config, content) {
|
||
const textContent = typeof content === 'object' ? content.text || '' : content;
|
||
|
||
return `
|
||
<div class="insertr-form-group">
|
||
<textarea class="insertr-form-textarea insertr-markdown-editor" name="content"
|
||
rows="${config.rows}"
|
||
placeholder="${config.placeholder}">${this.escapeHtml(textContent)}</textarea>
|
||
<div class="insertr-form-help">
|
||
Supports Markdown formatting (bold, italic, links, etc.)
|
||
</div>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
/**
|
||
* Create URL field for links
|
||
*/
|
||
createUrlField(content) {
|
||
const url = typeof content === 'object' ? content.url || '' : '';
|
||
|
||
return `
|
||
<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(url)}"
|
||
placeholder="https://example.com">
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
/**
|
||
* Setup event handlers
|
||
*/
|
||
setupEventHandlers(form, overlay, context, { onSave, onCancel }) {
|
||
const textarea = form.querySelector('textarea');
|
||
const urlInput = form.querySelector('input[name="url"]');
|
||
const saveBtn = form.querySelector('.insertr-btn-save');
|
||
const cancelBtn = form.querySelector('.insertr-btn-cancel');
|
||
const historyBtn = form.querySelector('.insertr-btn-history');
|
||
|
||
// Initialize previewer
|
||
this.previewer.setActiveContext(context);
|
||
|
||
// Setup live preview for content changes
|
||
if (textarea) {
|
||
textarea.addEventListener('input', () => {
|
||
const content = this.extractFormData(form);
|
||
this.previewer.schedulePreview(context, content);
|
||
});
|
||
}
|
||
|
||
// Setup live preview for URL changes (links only)
|
||
if (urlInput) {
|
||
urlInput.addEventListener('input', () => {
|
||
const content = this.extractFormData(form);
|
||
this.previewer.schedulePreview(context, content);
|
||
});
|
||
}
|
||
|
||
// Save handler
|
||
if (saveBtn) {
|
||
saveBtn.addEventListener('click', () => {
|
||
const content = this.extractFormData(form);
|
||
|
||
// Apply final content to elements
|
||
context.applyContent(content);
|
||
|
||
// Update stored original content to match current state
|
||
// This makes the saved content the new baseline for future edits
|
||
context.updateOriginalContent();
|
||
|
||
// Clear preview styling (won't restore content since original matches current)
|
||
this.previewer.clearPreview();
|
||
|
||
// Callback with the content
|
||
onSave(content);
|
||
this.close();
|
||
});
|
||
}
|
||
|
||
// Cancel handler
|
||
if (cancelBtn) {
|
||
cancelBtn.addEventListener('click', () => {
|
||
this.previewer.clearPreview();
|
||
onCancel();
|
||
this.close();
|
||
});
|
||
}
|
||
|
||
// History handler
|
||
if (historyBtn) {
|
||
historyBtn.addEventListener('click', () => {
|
||
const contentId = historyBtn.getAttribute('data-content-id');
|
||
console.log('Version history not implemented yet for:', contentId);
|
||
// TODO: Implement version history integration
|
||
});
|
||
}
|
||
|
||
// ESC key handler
|
||
const keyHandler = (e) => {
|
||
if (e.key === 'Escape') {
|
||
this.previewer.clearPreview();
|
||
onCancel();
|
||
this.close();
|
||
document.removeEventListener('keydown', keyHandler);
|
||
}
|
||
};
|
||
document.addEventListener('keydown', keyHandler);
|
||
|
||
// Click outside handler
|
||
overlay.addEventListener('click', (e) => {
|
||
if (e.target === overlay) {
|
||
this.previewer.clearPreview();
|
||
onCancel();
|
||
this.close();
|
||
}
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Extract form data consistently
|
||
*/
|
||
extractFormData(form) {
|
||
const textarea = form.querySelector('textarea[name="content"]');
|
||
const urlInput = form.querySelector('input[name="url"]');
|
||
|
||
const content = textarea ? textarea.value : '';
|
||
|
||
if (urlInput) {
|
||
// Link content
|
||
return {
|
||
text: content,
|
||
url: urlInput.value
|
||
};
|
||
}
|
||
|
||
// Regular content
|
||
return content;
|
||
}
|
||
|
||
/**
|
||
* Create overlay with backdrop
|
||
*/
|
||
createOverlay(form) {
|
||
const overlay = document.createElement('div');
|
||
overlay.className = 'insertr-form-overlay';
|
||
overlay.appendChild(form);
|
||
return overlay;
|
||
}
|
||
|
||
/**
|
||
* Position form relative to primary element
|
||
*/
|
||
positionForm(element, overlay) {
|
||
const rect = element.getBoundingClientRect();
|
||
const form = overlay.querySelector('.insertr-edit-form');
|
||
const viewportWidth = window.innerWidth;
|
||
|
||
// Calculate optimal width
|
||
let formWidth;
|
||
if (viewportWidth < 768) {
|
||
formWidth = Math.min(viewportWidth - 40, 500);
|
||
} else {
|
||
const minComfortableWidth = 600;
|
||
const maxWidth = Math.min(viewportWidth * 0.9, 800);
|
||
formWidth = Math.max(minComfortableWidth, Math.min(rect.width * 1.5, maxWidth));
|
||
}
|
||
|
||
form.style.width = `${formWidth}px`;
|
||
|
||
// Position below element
|
||
const top = rect.bottom + window.scrollY + 10;
|
||
const centerLeft = rect.left + window.scrollX + (rect.width / 2) - (formWidth / 2);
|
||
const minLeft = 20;
|
||
const maxLeft = window.innerWidth - formWidth - 20;
|
||
const left = Math.max(minLeft, Math.min(centerLeft, maxLeft));
|
||
|
||
overlay.style.position = 'absolute';
|
||
overlay.style.top = `${top}px`;
|
||
overlay.style.left = `${left}px`;
|
||
overlay.style.zIndex = '10000';
|
||
|
||
// Ensure visibility
|
||
this.ensureModalVisible(overlay);
|
||
}
|
||
|
||
/**
|
||
* Ensure modal is visible by scrolling if needed
|
||
*/
|
||
ensureModalVisible(overlay) {
|
||
requestAnimationFrame(() => {
|
||
const modal = overlay.querySelector('.insertr-edit-form');
|
||
const modalRect = modal.getBoundingClientRect();
|
||
const viewportHeight = window.innerHeight;
|
||
|
||
if (modalRect.bottom > viewportHeight) {
|
||
const scrollAmount = modalRect.bottom - viewportHeight + 20;
|
||
window.scrollBy({
|
||
top: scrollAmount,
|
||
behavior: 'smooth'
|
||
});
|
||
}
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Close current editor
|
||
*/
|
||
close() {
|
||
if (this.previewer) {
|
||
this.previewer.clearPreview();
|
||
}
|
||
|
||
if (this.currentOverlay) {
|
||
this.currentOverlay.remove();
|
||
this.currentOverlay = null;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Escape HTML to prevent XSS
|
||
*/
|
||
escapeHtml(text) {
|
||
if (typeof text !== 'string') return '';
|
||
const div = document.createElement('div');
|
||
div.textContent = text;
|
||
return div.innerHTML;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* EditContext - Represents content elements for editing
|
||
*/
|
||
class EditContext {
|
||
constructor(elements, currentContent) {
|
||
this.elements = elements;
|
||
this.primaryElement = elements[0];
|
||
this.originalContent = null;
|
||
this.currentContent = currentContent;
|
||
}
|
||
|
||
/**
|
||
* Extract content from elements in markdown format
|
||
*/
|
||
extractContent() {
|
||
if (this.elements.length === 1) {
|
||
const element = this.elements[0];
|
||
|
||
// Handle links specially
|
||
if (element.tagName.toLowerCase() === 'a') {
|
||
return {
|
||
text: markdownConverter.htmlToMarkdown(element.innerHTML),
|
||
url: element.href
|
||
};
|
||
}
|
||
|
||
// Single element - convert to markdown
|
||
return markdownConverter.htmlToMarkdown(element.innerHTML);
|
||
} else {
|
||
// Multiple elements - use group extraction
|
||
return markdownConverter.extractGroupMarkdown(this.elements);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Apply content to elements from markdown/object
|
||
*/
|
||
applyContent(content) {
|
||
if (this.elements.length === 1) {
|
||
const element = this.elements[0];
|
||
|
||
// Handle links specially
|
||
if (element.tagName.toLowerCase() === 'a' && typeof content === 'object') {
|
||
element.innerHTML = markdownConverter.markdownToHtml(content.text || '');
|
||
if (content.url) {
|
||
element.href = content.url;
|
||
}
|
||
return;
|
||
}
|
||
|
||
// Single element - convert markdown to HTML
|
||
const html = markdownConverter.markdownToHtml(content);
|
||
element.innerHTML = html;
|
||
} else {
|
||
// Multiple elements - use group update
|
||
markdownConverter.updateGroupElements(this.elements, content);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Store original content for preview restoration
|
||
*/
|
||
storeOriginalContent() {
|
||
this.originalContent = this.elements.map(el => ({
|
||
innerHTML: el.innerHTML,
|
||
href: el.href // Store href for links
|
||
}));
|
||
}
|
||
|
||
/**
|
||
* Restore original content (for preview cancellation)
|
||
*/
|
||
restoreOriginalContent() {
|
||
if (this.originalContent) {
|
||
this.elements.forEach((el, index) => {
|
||
if (this.originalContent[index] !== undefined) {
|
||
el.innerHTML = this.originalContent[index].innerHTML;
|
||
if (this.originalContent[index].href) {
|
||
el.href = this.originalContent[index].href;
|
||
}
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Update original content to match current element state (after save)
|
||
* This makes the current content the new baseline for future cancellations
|
||
*/
|
||
updateOriginalContent() {
|
||
this.originalContent = this.elements.map(el => ({
|
||
innerHTML: el.innerHTML,
|
||
href: el.href // Store href for links
|
||
}));
|
||
}
|
||
|
||
/**
|
||
* Apply preview styling to all elements
|
||
*/
|
||
applyPreviewStyling() {
|
||
this.elements.forEach(el => {
|
||
el.classList.add('insertr-preview-active');
|
||
});
|
||
|
||
// Also apply to containers if they're groups
|
||
if (this.primaryElement.classList.contains('insertr-group')) {
|
||
this.primaryElement.classList.add('insertr-preview-active');
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Remove preview styling from all elements
|
||
*/
|
||
removePreviewStyling() {
|
||
this.elements.forEach(el => {
|
||
el.classList.remove('insertr-preview-active');
|
||
});
|
||
|
||
// Also remove from containers
|
||
if (this.primaryElement.classList.contains('insertr-group')) {
|
||
this.primaryElement.classList.remove('insertr-preview-active');
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* InsertrFormRenderer - Form renderer using markdown-first approach
|
||
* Thin wrapper around the Editor system
|
||
*/
|
||
|
||
class InsertrFormRenderer {
|
||
constructor(apiClient = null) {
|
||
this.apiClient = apiClient;
|
||
this.editor = new Editor();
|
||
this.setupStyles();
|
||
}
|
||
|
||
/**
|
||
* Show edit form for any content element
|
||
* @param {Object} meta - Element metadata {element, contentId, contentType}
|
||
* @param {string|Object} currentContent - Current content value
|
||
* @param {Function} onSave - Save callback
|
||
* @param {Function} onCancel - Cancel callback
|
||
*/
|
||
showEditForm(meta, currentContent, onSave, onCancel) {
|
||
const { element } = meta;
|
||
|
||
// Handle insertr-group elements by getting their viable children
|
||
if (element.classList.contains('insertr-group')) {
|
||
const children = this.getGroupChildren(element);
|
||
const groupMeta = { ...meta, element: children };
|
||
return this.editor.edit(groupMeta, currentContent, onSave, onCancel);
|
||
}
|
||
|
||
// All other elements use the editor directly
|
||
return this.editor.edit(meta, currentContent, onSave, onCancel);
|
||
}
|
||
|
||
/**
|
||
* Get viable children from group element
|
||
*/
|
||
getGroupChildren(groupElement) {
|
||
const children = [];
|
||
for (const child of groupElement.children) {
|
||
// Skip elements that don't have meaningful text content
|
||
if (child.textContent.trim().length > 0) {
|
||
children.push(child);
|
||
}
|
||
}
|
||
return children;
|
||
}
|
||
|
||
/**
|
||
* Close current form
|
||
*/
|
||
closeForm() {
|
||
this.editor.close();
|
||
}
|
||
|
||
/**
|
||
* Show version history modal (placeholder for future implementation)
|
||
*/
|
||
async showVersionHistory(contentId, element, onRestore) {
|
||
try {
|
||
// Get version history from API
|
||
const apiClient = this.getApiClient();
|
||
if (!apiClient) {
|
||
console.warn('No API client configured for version history');
|
||
return;
|
||
}
|
||
|
||
const versions = await apiClient.getContentVersions(contentId);
|
||
|
||
// Create version history modal
|
||
const historyModal = this.createVersionHistoryModal(contentId, versions, onRestore);
|
||
document.body.appendChild(historyModal);
|
||
|
||
// Setup handlers
|
||
this.setupVersionHistoryHandlers(historyModal, contentId);
|
||
|
||
} catch (error) {
|
||
console.error('Failed to load version history:', error);
|
||
this.showVersionHistoryError('Failed to load version history. Please try again.');
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Create version history modal (simplified placeholder)
|
||
*/
|
||
createVersionHistoryModal(contentId, versions, onRestore) {
|
||
const modal = document.createElement('div');
|
||
modal.className = 'insertr-version-modal';
|
||
|
||
let versionsHTML = '';
|
||
if (versions && versions.length > 0) {
|
||
versionsHTML = versions.map((version, index) => `
|
||
<div class="insertr-version-item" data-version-id="${version.version_id}">
|
||
<div class="insertr-version-meta">
|
||
<span class="insertr-version-label">${index === 0 ? 'Previous Version' : `Version ${versions.length - index}`}</span>
|
||
<span class="insertr-version-date">${this.formatDate(version.created_at)}</span>
|
||
${version.created_by ? `<span class="insertr-version-user">by ${version.created_by}</span>` : ''}
|
||
</div>
|
||
<div class="insertr-version-content">${this.escapeHtml(this.truncateContent(version.value, 100))}</div>
|
||
<div class="insertr-version-actions">
|
||
<button type="button" class="insertr-btn-restore" data-version-id="${version.version_id}">Restore</button>
|
||
<button type="button" class="insertr-btn-view-diff" data-version-id="${version.version_id}">View Full</button>
|
||
</div>
|
||
</div>
|
||
`).join('');
|
||
} else {
|
||
versionsHTML = '<div class="insertr-version-empty">No previous versions found</div>';
|
||
}
|
||
|
||
modal.innerHTML = `
|
||
<div class="insertr-version-backdrop">
|
||
<div class="insertr-version-content-modal">
|
||
<div class="insertr-version-header">
|
||
<h3>Version History</h3>
|
||
<button type="button" class="insertr-btn-close">×</button>
|
||
</div>
|
||
<div class="insertr-version-list">
|
||
${versionsHTML}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
return modal;
|
||
}
|
||
|
||
/**
|
||
* Setup version history modal handlers
|
||
*/
|
||
setupVersionHistoryHandlers(modal, contentId) {
|
||
const closeBtn = modal.querySelector('.insertr-btn-close');
|
||
const backdrop = modal.querySelector('.insertr-version-backdrop');
|
||
|
||
// Close handlers
|
||
if (closeBtn) {
|
||
closeBtn.addEventListener('click', () => modal.remove());
|
||
}
|
||
|
||
backdrop.addEventListener('click', (e) => {
|
||
if (e.target === backdrop) {
|
||
modal.remove();
|
||
}
|
||
});
|
||
|
||
// Restore handlers
|
||
const restoreButtons = modal.querySelectorAll('.insertr-btn-restore');
|
||
restoreButtons.forEach(btn => {
|
||
btn.addEventListener('click', async () => {
|
||
const versionId = btn.getAttribute('data-version-id');
|
||
if (await this.confirmRestore()) {
|
||
await this.restoreVersion(contentId, versionId);
|
||
modal.remove();
|
||
this.closeForm();
|
||
}
|
||
});
|
||
});
|
||
|
||
// View diff handlers
|
||
const viewButtons = modal.querySelectorAll('.insertr-btn-view-diff');
|
||
viewButtons.forEach(btn => {
|
||
btn.addEventListener('click', () => {
|
||
const versionId = btn.getAttribute('data-version-id');
|
||
this.showVersionDetails(versionId);
|
||
});
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Helper methods for version history
|
||
*/
|
||
formatDate(dateString) {
|
||
const date = new Date(dateString);
|
||
const now = new Date();
|
||
const diff = now - date;
|
||
|
||
// Less than 24 hours ago
|
||
if (diff < 24 * 60 * 60 * 1000) {
|
||
const hours = Math.floor(diff / (60 * 60 * 1000));
|
||
if (hours < 1) {
|
||
const minutes = Math.floor(diff / (60 * 1000));
|
||
return `${minutes}m ago`;
|
||
}
|
||
return `${hours}h ago`;
|
||
}
|
||
|
||
// Less than 7 days ago
|
||
if (diff < 7 * 24 * 60 * 60 * 1000) {
|
||
const days = Math.floor(diff / (24 * 60 * 60 * 1000));
|
||
return `${days}d ago`;
|
||
}
|
||
|
||
// Older - show actual date
|
||
return date.toLocaleDateString();
|
||
}
|
||
|
||
truncateContent(content, maxLength) {
|
||
if (content.length <= maxLength) return content;
|
||
return content.substring(0, maxLength) + '...';
|
||
}
|
||
|
||
async confirmRestore() {
|
||
return confirm('Are you sure you want to restore this version? This will replace the current content.');
|
||
}
|
||
|
||
async restoreVersion(contentId, versionId) {
|
||
try {
|
||
const apiClient = this.getApiClient();
|
||
await apiClient.rollbackContent(contentId, versionId);
|
||
return true;
|
||
} catch (error) {
|
||
console.error('Failed to restore version:', error);
|
||
alert('Failed to restore version. Please try again.');
|
||
return false;
|
||
}
|
||
}
|
||
|
||
showVersionDetails(versionId) {
|
||
// TODO: Implement detailed version view with diff
|
||
alert(`Version details not implemented yet (Version ID: ${versionId})`);
|
||
}
|
||
|
||
showVersionHistoryError(message) {
|
||
alert(message);
|
||
}
|
||
|
||
// Helper to get API client
|
||
getApiClient() {
|
||
return this.apiClient || window.insertrAPIClient || null;
|
||
}
|
||
|
||
/**
|
||
* Escape HTML to prevent XSS
|
||
*/
|
||
escapeHtml(text) {
|
||
if (typeof text !== 'string') return '';
|
||
const div = document.createElement('div');
|
||
div.textContent = text;
|
||
return div.innerHTML;
|
||
}
|
||
|
||
/**
|
||
* Setup form styles (consolidated and simplified)
|
||
*/
|
||
setupStyles() {
|
||
const styles = `
|
||
/* Overlay and Form Container */
|
||
.insertr-form-overlay {
|
||
position: absolute;
|
||
z-index: 10000;
|
||
}
|
||
|
||
.insertr-edit-form {
|
||
background: white;
|
||
border: 2px solid #007cba;
|
||
border-radius: 8px;
|
||
padding: 1rem;
|
||
box-shadow: 0 8px 25px rgba(0,0,0,0.15);
|
||
width: 100%;
|
||
box-sizing: border-box;
|
||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||
min-width: 600px;
|
||
max-width: 800px;
|
||
}
|
||
|
||
/* Form Header */
|
||
.insertr-form-header {
|
||
font-weight: 600;
|
||
color: #1f2937;
|
||
margin-bottom: 1rem;
|
||
padding-bottom: 0.5rem;
|
||
border-bottom: 1px solid #e5e7eb;
|
||
font-size: 0.875rem;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.5px;
|
||
}
|
||
|
||
/* Form Groups and Fields */
|
||
.insertr-form-group {
|
||
margin-bottom: 1rem;
|
||
}
|
||
|
||
.insertr-form-group:last-child {
|
||
margin-bottom: 0;
|
||
}
|
||
|
||
.insertr-form-label {
|
||
display: block;
|
||
font-weight: 600;
|
||
color: #374151;
|
||
margin-bottom: 0.5rem;
|
||
font-size: 0.875rem;
|
||
}
|
||
|
||
.insertr-form-input,
|
||
.insertr-form-textarea {
|
||
width: 100%;
|
||
padding: 0.75rem;
|
||
border: 1px solid #d1d5db;
|
||
border-radius: 6px;
|
||
font-family: inherit;
|
||
font-size: 1rem;
|
||
transition: border-color 0.2s, box-shadow 0.2s;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.insertr-form-input:focus,
|
||
.insertr-form-textarea:focus {
|
||
outline: none;
|
||
border-color: #007cba;
|
||
box-shadow: 0 0 0 3px rgba(0, 124, 186, 0.1);
|
||
}
|
||
|
||
/* Markdown Editor Styling */
|
||
.insertr-form-textarea {
|
||
min-height: 120px;
|
||
resize: vertical;
|
||
font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace;
|
||
}
|
||
|
||
.insertr-markdown-editor {
|
||
min-height: 200px;
|
||
font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace;
|
||
font-size: 0.9rem;
|
||
line-height: 1.5;
|
||
background-color: #f8fafc;
|
||
}
|
||
|
||
/* Form Actions */
|
||
.insertr-form-actions {
|
||
display: flex;
|
||
gap: 0.5rem;
|
||
justify-content: flex-end;
|
||
margin-top: 1rem;
|
||
padding-top: 1rem;
|
||
border-top: 1px solid #e5e7eb;
|
||
}
|
||
|
||
.insertr-btn-save {
|
||
background: #10b981;
|
||
color: white;
|
||
border: none;
|
||
padding: 0.5rem 1rem;
|
||
border-radius: 6px;
|
||
font-weight: 500;
|
||
cursor: pointer;
|
||
transition: background-color 0.2s;
|
||
font-size: 0.875rem;
|
||
}
|
||
|
||
.insertr-btn-save:hover {
|
||
background: #059669;
|
||
}
|
||
|
||
.insertr-btn-cancel {
|
||
background: #6b7280;
|
||
color: white;
|
||
border: none;
|
||
padding: 0.5rem 1rem;
|
||
border-radius: 6px;
|
||
font-weight: 500;
|
||
cursor: pointer;
|
||
transition: background-color 0.2s;
|
||
font-size: 0.875rem;
|
||
}
|
||
|
||
.insertr-btn-cancel:hover {
|
||
background: #4b5563;
|
||
}
|
||
|
||
.insertr-btn-history {
|
||
background: #6f42c1;
|
||
color: white;
|
||
border: none;
|
||
padding: 0.5rem 1rem;
|
||
border-radius: 6px;
|
||
font-weight: 500;
|
||
cursor: pointer;
|
||
transition: background-color 0.2s;
|
||
font-size: 0.875rem;
|
||
}
|
||
|
||
.insertr-btn-history:hover {
|
||
background: #5a359a;
|
||
}
|
||
|
||
.insertr-form-help {
|
||
font-size: 0.75rem;
|
||
color: #6b7280;
|
||
margin-top: 0.25rem;
|
||
}
|
||
|
||
/* Live Preview Styles */
|
||
.insertr-preview-active {
|
||
position: relative;
|
||
background: rgba(0, 124, 186, 0.05) !important;
|
||
outline: 2px solid #007cba !important;
|
||
outline-offset: 2px;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.insertr-preview-active::after {
|
||
content: "Preview";
|
||
position: absolute;
|
||
top: -25px;
|
||
left: 0;
|
||
background: #007cba;
|
||
color: white;
|
||
padding: 2px 8px;
|
||
border-radius: 3px;
|
||
font-size: 0.75rem;
|
||
font-weight: 500;
|
||
z-index: 10001;
|
||
white-space: nowrap;
|
||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||
}
|
||
|
||
/* Responsive Design */
|
||
@media (max-width: 768px) {
|
||
.insertr-edit-form {
|
||
min-width: 90vw;
|
||
max-width: 90vw;
|
||
}
|
||
|
||
.insertr-preview-active::after {
|
||
top: -20px;
|
||
font-size: 0.7rem;
|
||
padding: 1px 6px;
|
||
}
|
||
}
|
||
|
||
/* Version History Modal Styles */
|
||
.insertr-version-modal {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
z-index: 10001;
|
||
}
|
||
|
||
.insertr-version-backdrop {
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
background-color: rgba(0, 0, 0, 0.5);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: 20px;
|
||
}
|
||
|
||
.insertr-version-content-modal {
|
||
background: white;
|
||
border-radius: 8px;
|
||
max-width: 600px;
|
||
width: 100%;
|
||
max-height: 80vh;
|
||
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2);
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.insertr-version-header {
|
||
padding: 20px 20px 0;
|
||
border-bottom: 1px solid #eee;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.insertr-version-header h3 {
|
||
margin: 0 0 20px;
|
||
color: #333;
|
||
font-size: 18px;
|
||
}
|
||
|
||
.insertr-btn-close {
|
||
background: none;
|
||
border: none;
|
||
font-size: 24px;
|
||
cursor: pointer;
|
||
color: #666;
|
||
padding: 0;
|
||
width: 30px;
|
||
height: 30px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.insertr-btn-close:hover {
|
||
color: #333;
|
||
}
|
||
|
||
.insertr-version-list {
|
||
overflow-y: auto;
|
||
padding: 20px;
|
||
flex: 1;
|
||
}
|
||
|
||
.insertr-version-item {
|
||
border: 1px solid #e1e5e9;
|
||
border-radius: 6px;
|
||
padding: 16px;
|
||
margin-bottom: 12px;
|
||
background: #f8f9fa;
|
||
}
|
||
|
||
.insertr-version-meta {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
margin-bottom: 8px;
|
||
font-size: 13px;
|
||
}
|
||
|
||
.insertr-version-label {
|
||
font-weight: 600;
|
||
color: #0969da;
|
||
}
|
||
|
||
.insertr-version-date {
|
||
color: #656d76;
|
||
}
|
||
|
||
.insertr-version-user {
|
||
color: #656d76;
|
||
}
|
||
|
||
.insertr-version-content {
|
||
margin-bottom: 12px;
|
||
padding: 8px;
|
||
background: white;
|
||
border-radius: 4px;
|
||
font-family: monospace;
|
||
font-size: 14px;
|
||
color: #24292f;
|
||
white-space: pre-wrap;
|
||
}
|
||
|
||
.insertr-version-actions {
|
||
display: flex;
|
||
gap: 8px;
|
||
}
|
||
|
||
.insertr-btn-restore {
|
||
background: #0969da;
|
||
color: white;
|
||
border: none;
|
||
padding: 6px 12px;
|
||
border-radius: 4px;
|
||
cursor: pointer;
|
||
font-size: 12px;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.insertr-btn-restore:hover {
|
||
background: #0860ca;
|
||
}
|
||
|
||
.insertr-btn-view-diff {
|
||
background: #f6f8fa;
|
||
color: #24292f;
|
||
border: 1px solid #d1d9e0;
|
||
padding: 6px 12px;
|
||
border-radius: 4px;
|
||
cursor: pointer;
|
||
font-size: 12px;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.insertr-btn-view-diff:hover {
|
||
background: #f3f4f6;
|
||
}
|
||
|
||
.insertr-version-empty {
|
||
text-align: center;
|
||
color: #656d76;
|
||
font-style: italic;
|
||
padding: 40px 20px;
|
||
}
|
||
`;
|
||
|
||
const styleSheet = document.createElement('style');
|
||
styleSheet.type = 'text/css';
|
||
styleSheet.innerHTML = styles;
|
||
document.head.appendChild(styleSheet);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* InsertrEditor - Visual editing functionality
|
||
*/
|
||
class InsertrEditor {
|
||
constructor(core, auth, apiClient, options = {}) {
|
||
this.core = core;
|
||
this.auth = auth;
|
||
this.apiClient = apiClient;
|
||
this.options = options;
|
||
this.isActive = false;
|
||
this.formRenderer = new InsertrFormRenderer(apiClient);
|
||
}
|
||
|
||
start() {
|
||
if (this.isActive) return;
|
||
|
||
console.log('🚀 Starting Insertr Editor');
|
||
this.isActive = true;
|
||
|
||
// Add editor styles
|
||
this.addEditorStyles();
|
||
|
||
// Initialize all enhanced elements
|
||
const elements = this.core.getAllElements();
|
||
console.log(`📝 Found ${elements.length} editable elements`);
|
||
|
||
elements.forEach(meta => this.initializeElement(meta));
|
||
}
|
||
|
||
initializeElement(meta) {
|
||
const { element, contentId, contentType } = meta;
|
||
|
||
// Add visual indicators
|
||
element.style.cursor = 'pointer';
|
||
element.style.position = 'relative';
|
||
|
||
// Add interaction handlers
|
||
this.addHoverEffects(element);
|
||
this.addClickHandler(element, meta);
|
||
}
|
||
|
||
addHoverEffects(element) {
|
||
element.addEventListener('mouseenter', () => {
|
||
element.classList.add('insertr-editing-hover');
|
||
});
|
||
|
||
element.addEventListener('mouseleave', () => {
|
||
element.classList.remove('insertr-editing-hover');
|
||
});
|
||
}
|
||
|
||
addClickHandler(element, meta) {
|
||
element.addEventListener('click', (e) => {
|
||
// Only allow editing if authenticated and in edit mode
|
||
if (!this.auth.isAuthenticated() || !this.auth.isEditMode()) {
|
||
return; // Let normal click behavior happen
|
||
}
|
||
|
||
e.preventDefault();
|
||
this.openEditor(meta);
|
||
});
|
||
}
|
||
|
||
openEditor(meta) {
|
||
const { element } = meta;
|
||
const currentContent = this.extractCurrentContent(element);
|
||
|
||
// Show professional form instead of prompt
|
||
this.formRenderer.showEditForm(
|
||
meta,
|
||
currentContent,
|
||
(formData) => this.handleSave(meta, formData),
|
||
() => this.handleCancel(meta)
|
||
);
|
||
}
|
||
|
||
extractCurrentContent(element) {
|
||
// For links, extract both text and URL
|
||
if (element.tagName.toLowerCase() === 'a') {
|
||
return {
|
||
text: element.textContent.trim(),
|
||
url: element.getAttribute('href') || ''
|
||
};
|
||
}
|
||
|
||
// For other elements, just return text content
|
||
return element.textContent.trim();
|
||
}
|
||
|
||
async handleSave(meta, formData) {
|
||
console.log('💾 Saving content:', meta.contentId, formData);
|
||
|
||
try {
|
||
// Extract content value based on type
|
||
let contentValue;
|
||
if (meta.element.tagName.toLowerCase() === 'a') {
|
||
// For links, save the text content (URL is handled separately if needed)
|
||
contentValue = formData.text || formData;
|
||
} else {
|
||
contentValue = formData.text || formData;
|
||
}
|
||
|
||
// Universal upsert - server handles ID extraction/generation from markup
|
||
const contentType = this.determineContentType(meta.element);
|
||
const result = await this.apiClient.createContent(
|
||
contentValue,
|
||
contentType,
|
||
meta.htmlMarkup // Always send HTML markup - server is smart about ID handling
|
||
);
|
||
|
||
if (result) {
|
||
// Store the backend-generated/confirmed ID in the element
|
||
meta.element.setAttribute('data-content-id', result.id);
|
||
meta.element.setAttribute('data-content-type', result.type);
|
||
console.log(`✅ Content saved: ${result.id}`, contentValue);
|
||
} else {
|
||
console.error('❌ Failed to save content to server');
|
||
}
|
||
|
||
// Close form
|
||
this.formRenderer.closeForm();
|
||
|
||
} catch (error) {
|
||
console.error('❌ Error saving content:', error);
|
||
this.formRenderer.closeForm();
|
||
}
|
||
}
|
||
|
||
determineContentType(element) {
|
||
const tagName = element.tagName.toLowerCase();
|
||
|
||
if (tagName === 'a' || tagName === 'button') {
|
||
return 'link';
|
||
}
|
||
|
||
// ALL text elements use markdown for consistent editing experience
|
||
return 'markdown';
|
||
}
|
||
|
||
handleCancel(meta) {
|
||
console.log('❌ Edit cancelled:', meta.contentId);
|
||
}
|
||
|
||
|
||
addEditorStyles() {
|
||
const styles = `
|
||
.insertr-editing-hover {
|
||
outline: 2px dashed #007cba !important;
|
||
outline-offset: 2px !important;
|
||
background-color: rgba(0, 124, 186, 0.05) !important;
|
||
}
|
||
|
||
.insertr:hover::after {
|
||
content: "✏️ " attr(data-content-type);
|
||
position: absolute;
|
||
top: -25px;
|
||
left: 0;
|
||
background: #007cba;
|
||
color: white;
|
||
padding: 2px 6px;
|
||
font-size: 11px;
|
||
border-radius: 3px;
|
||
white-space: nowrap;
|
||
z-index: 1000;
|
||
font-family: monospace;
|
||
}
|
||
|
||
/* Version History Modal Styles */
|
||
.insertr-version-modal {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
z-index: 10001;
|
||
}
|
||
|
||
.insertr-version-backdrop {
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
background-color: rgba(0, 0, 0, 0.5);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: 20px;
|
||
}
|
||
|
||
.insertr-version-content-modal {
|
||
background: white;
|
||
border-radius: 8px;
|
||
max-width: 600px;
|
||
width: 100%;
|
||
max-height: 80vh;
|
||
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2);
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.insertr-version-header {
|
||
padding: 20px 20px 0;
|
||
border-bottom: 1px solid #eee;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.insertr-version-header h3 {
|
||
margin: 0 0 20px;
|
||
color: #333;
|
||
font-size: 18px;
|
||
}
|
||
|
||
.insertr-btn-close {
|
||
background: none;
|
||
border: none;
|
||
font-size: 24px;
|
||
cursor: pointer;
|
||
color: #666;
|
||
padding: 0;
|
||
width: 30px;
|
||
height: 30px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.insertr-btn-close:hover {
|
||
color: #333;
|
||
}
|
||
|
||
.insertr-version-list {
|
||
overflow-y: auto;
|
||
padding: 20px;
|
||
flex: 1;
|
||
}
|
||
|
||
.insertr-version-item {
|
||
border: 1px solid #e1e5e9;
|
||
border-radius: 6px;
|
||
padding: 16px;
|
||
margin-bottom: 12px;
|
||
background: #f8f9fa;
|
||
}
|
||
|
||
.insertr-version-meta {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
margin-bottom: 8px;
|
||
font-size: 13px;
|
||
}
|
||
|
||
.insertr-version-label {
|
||
font-weight: 600;
|
||
color: #0969da;
|
||
}
|
||
|
||
.insertr-version-date {
|
||
color: #656d76;
|
||
}
|
||
|
||
.insertr-version-user {
|
||
color: #656d76;
|
||
}
|
||
|
||
.insertr-version-content {
|
||
margin-bottom: 12px;
|
||
padding: 8px;
|
||
background: white;
|
||
border-radius: 4px;
|
||
font-family: monospace;
|
||
font-size: 14px;
|
||
color: #24292f;
|
||
white-space: pre-wrap;
|
||
}
|
||
|
||
.insertr-version-actions {
|
||
display: flex;
|
||
gap: 8px;
|
||
}
|
||
|
||
.insertr-btn-restore {
|
||
background: #0969da;
|
||
color: white;
|
||
border: none;
|
||
padding: 6px 12px;
|
||
border-radius: 4px;
|
||
cursor: pointer;
|
||
font-size: 12px;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.insertr-btn-restore:hover {
|
||
background: #0860ca;
|
||
}
|
||
|
||
.insertr-btn-view-diff {
|
||
background: #f6f8fa;
|
||
color: #24292f;
|
||
border: 1px solid #d1d9e0;
|
||
padding: 6px 12px;
|
||
border-radius: 4px;
|
||
cursor: pointer;
|
||
font-size: 12px;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.insertr-btn-view-diff:hover {
|
||
background: #f3f4f6;
|
||
}
|
||
|
||
.insertr-version-empty {
|
||
text-align: center;
|
||
color: #656d76;
|
||
font-style: italic;
|
||
padding: 40px 20px;
|
||
}
|
||
|
||
/* History Button in Form */
|
||
.insertr-btn-history {
|
||
background: #6f42c1;
|
||
color: white;
|
||
border: none;
|
||
padding: 8px 16px;
|
||
border-radius: 4px;
|
||
cursor: pointer;
|
||
font-size: 14px;
|
||
font-weight: 500;
|
||
margin-left: auto;
|
||
}
|
||
|
||
.insertr-btn-history:hover {
|
||
background: #5a359a;
|
||
}
|
||
`;
|
||
|
||
const styleSheet = document.createElement('style');
|
||
styleSheet.type = 'text/css';
|
||
styleSheet.innerHTML = styles;
|
||
document.head.appendChild(styleSheet);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* InsertrAuth - Authentication and state management
|
||
* Handles user authentication, edit mode, and visual state changes
|
||
*/
|
||
class InsertrAuth {
|
||
constructor(options = {}) {
|
||
this.options = {
|
||
mockAuth: options.mockAuth !== false, // Enable mock auth by default
|
||
hideGatesAfterAuth: options.hideGatesAfterAuth === true, // Keep gates visible by default
|
||
...options
|
||
};
|
||
|
||
// Authentication state
|
||
this.state = {
|
||
isAuthenticated: false,
|
||
editMode: false,
|
||
currentUser: null,
|
||
activeEditor: null,
|
||
isInitialized: false,
|
||
isAuthenticating: false
|
||
};
|
||
|
||
this.statusIndicator = null;
|
||
}
|
||
|
||
/**
|
||
* Initialize gate system (called on page load)
|
||
*/
|
||
init() {
|
||
console.log('🔧 Insertr: Scanning for editor gates');
|
||
|
||
this.setupEditorGates();
|
||
}
|
||
|
||
/**
|
||
* Initialize full editing system (called after successful OAuth)
|
||
*/
|
||
initializeFullSystem() {
|
||
if (this.state.isInitialized) {
|
||
return; // Already initialized
|
||
}
|
||
|
||
console.log('🔐 Initializing Insertr Editing System');
|
||
|
||
this.createAuthControls();
|
||
this.setupAuthenticationControls();
|
||
this.createStatusIndicator();
|
||
this.updateBodyClasses();
|
||
|
||
// Auto-enable edit mode after OAuth
|
||
this.state.editMode = true;
|
||
this.state.isInitialized = true;
|
||
|
||
// Start the editor system
|
||
if (window.Insertr && window.Insertr.startEditor) {
|
||
window.Insertr.startEditor();
|
||
}
|
||
|
||
this.updateButtonStates();
|
||
this.updateStatusIndicator();
|
||
|
||
console.log('📱 Editing system active - Controls in bottom-right corner');
|
||
console.log('✏️ Edit mode enabled - Click elements to edit');
|
||
}
|
||
|
||
/**
|
||
* Setup editor gate click handlers for any .insertr-gate elements
|
||
*/
|
||
setupEditorGates() {
|
||
const gates = document.querySelectorAll('.insertr-gate');
|
||
|
||
if (gates.length === 0) {
|
||
console.log('ℹ️ No .insertr-gate elements found - editor access disabled');
|
||
return;
|
||
}
|
||
|
||
console.log(`🚪 Found ${gates.length} editor gate(s)`);
|
||
|
||
// Add gate styles
|
||
this.addGateStyles();
|
||
|
||
gates.forEach((gate, index) => {
|
||
// Store original text for later restoration
|
||
if (!gate.hasAttribute('data-original-text')) {
|
||
gate.setAttribute('data-original-text', gate.textContent);
|
||
}
|
||
|
||
gate.addEventListener('click', (e) => {
|
||
e.preventDefault();
|
||
this.handleGateClick(gate, index);
|
||
});
|
||
|
||
// Add subtle styling to indicate it's clickable
|
||
gate.style.cursor = 'pointer';
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Handle click on an editor gate element
|
||
*/
|
||
async handleGateClick(gateElement, gateIndex) {
|
||
// Prevent multiple simultaneous authentication attempts
|
||
if (this.state.isAuthenticating) {
|
||
console.log('⏳ Authentication already in progress...');
|
||
return;
|
||
}
|
||
|
||
console.log(`🚀 Editor gate activated (gate ${gateIndex + 1})`);
|
||
this.state.isAuthenticating = true;
|
||
|
||
// Store original text and show loading state
|
||
const originalText = gateElement.textContent;
|
||
gateElement.setAttribute('data-original-text', originalText);
|
||
gateElement.textContent = '⏳ Signing in...';
|
||
gateElement.style.pointerEvents = 'none';
|
||
|
||
try {
|
||
// Perform OAuth authentication
|
||
await this.performOAuthFlow();
|
||
|
||
// Initialize full editing system
|
||
this.initializeFullSystem();
|
||
|
||
// Conditionally hide gates based on options
|
||
if (this.options.hideGatesAfterAuth) {
|
||
this.hideAllGates();
|
||
} else {
|
||
this.updateGateState();
|
||
}
|
||
|
||
} catch (error) {
|
||
console.error('❌ Authentication failed:', error);
|
||
|
||
// Restore clicked gate to original state
|
||
const originalText = gateElement.getAttribute('data-original-text');
|
||
if (originalText) {
|
||
gateElement.textContent = originalText;
|
||
}
|
||
gateElement.style.pointerEvents = '';
|
||
} finally {
|
||
this.state.isAuthenticating = false;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Perform OAuth authentication flow
|
||
*/
|
||
async performOAuthFlow() {
|
||
// In development, simulate OAuth flow
|
||
if (this.options.mockAuth) {
|
||
console.log('🔐 Mock OAuth: Simulating authentication...');
|
||
|
||
// Simulate network delay
|
||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||
|
||
// Set authenticated state
|
||
this.state.isAuthenticated = true;
|
||
this.state.currentUser = {
|
||
name: 'Site Owner',
|
||
email: 'owner@example.com',
|
||
role: 'admin'
|
||
};
|
||
|
||
console.log('✅ Mock OAuth: Authentication successful');
|
||
return;
|
||
}
|
||
|
||
// TODO: In production, implement real OAuth flow
|
||
// This would redirect to OAuth provider, handle callback, etc.
|
||
throw new Error('Production OAuth not implemented yet');
|
||
}
|
||
|
||
/**
|
||
* Hide all editor gates after successful authentication (optional)
|
||
*/
|
||
hideAllGates() {
|
||
document.body.classList.add('insertr-hide-gates');
|
||
console.log('🚪 Editor gates hidden (hideGatesAfterAuth enabled)');
|
||
}
|
||
|
||
/**
|
||
* Update gate state after authentication (restore normal appearance)
|
||
*/
|
||
updateGateState() {
|
||
const gates = document.querySelectorAll('.insertr-gate');
|
||
gates.forEach(gate => {
|
||
// Restore original text if it was saved
|
||
const originalText = gate.getAttribute('data-original-text');
|
||
if (originalText) {
|
||
gate.textContent = originalText;
|
||
}
|
||
|
||
// Restore interactive state
|
||
gate.style.pointerEvents = '';
|
||
gate.style.opacity = '';
|
||
});
|
||
|
||
console.log('🚪 Editor gates restored to original state');
|
||
}
|
||
|
||
/**
|
||
* Create authentication control buttons (bottom-right positioned)
|
||
*/
|
||
createAuthControls() {
|
||
// Check if controls already exist
|
||
if (document.getElementById('insertr-auth-controls')) {
|
||
return;
|
||
}
|
||
|
||
const controlsHtml = `
|
||
<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' : ''}`;
|
||
}
|
||
|
||
// Update enhance button visibility
|
||
this.updateEnhanceButtonVisibility();
|
||
}
|
||
|
||
/**
|
||
* Create status indicator
|
||
*/
|
||
createStatusIndicator() {
|
||
// Check if already exists
|
||
if (document.getElementById('insertr-status')) {
|
||
return;
|
||
}
|
||
|
||
const statusHtml = `
|
||
<div id="insertr-status-controls" class="insertr-status-controls">
|
||
<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>
|
||
<button id="insertr-enhance-btn" class="insertr-auth-btn" style="display: none;" title="Enhance files with latest content">🔄 Enhance</button>
|
||
</div>
|
||
`;
|
||
|
||
document.body.insertAdjacentHTML('beforeend', statusHtml);
|
||
this.statusIndicator = document.getElementById('insertr-status');
|
||
this.setupEnhanceButton();
|
||
this.updateStatusIndicator();
|
||
}
|
||
|
||
/**
|
||
* Update status indicator text and style
|
||
*/
|
||
updateStatusIndicator() {
|
||
const statusText = document.querySelector('.insertr-status-text');
|
||
const statusDot = document.querySelector('.insertr-status-dot');
|
||
|
||
if (!statusText || !statusDot) return;
|
||
|
||
if (!this.state.isAuthenticated) {
|
||
statusText.textContent = 'Visitor Mode';
|
||
statusDot.className = 'insertr-status-dot insertr-status-visitor';
|
||
} else if (this.state.editMode) {
|
||
statusText.textContent = 'Editing';
|
||
statusDot.className = 'insertr-status-dot insertr-status-editing';
|
||
} else {
|
||
statusText.textContent = 'Authenticated';
|
||
statusDot.className = 'insertr-status-dot insertr-status-authenticated';
|
||
}
|
||
|
||
|
||
}
|
||
|
||
/**
|
||
* Check if user is authenticated
|
||
*/
|
||
isAuthenticated() {
|
||
return this.state.isAuthenticated;
|
||
}
|
||
|
||
/**
|
||
* Check if edit mode is enabled
|
||
*/
|
||
isEditMode() {
|
||
return this.state.editMode;
|
||
}
|
||
|
||
/**
|
||
* Get current user info
|
||
*/
|
||
getCurrentUser() {
|
||
return this.state.currentUser;
|
||
}
|
||
|
||
/**
|
||
* Add minimal styles for editor gates
|
||
*/
|
||
addGateStyles() {
|
||
const styles = `
|
||
.insertr-gate {
|
||
transition: opacity 0.2s ease;
|
||
user-select: none;
|
||
}
|
||
|
||
.insertr-gate:hover {
|
||
opacity: 0.7;
|
||
}
|
||
|
||
/* Optional: Hide gates when authenticated (only if hideGatesAfterAuth option is true) */
|
||
body.insertr-hide-gates .insertr-gate {
|
||
display: none !important;
|
||
}
|
||
`;
|
||
|
||
const styleSheet = document.createElement('style');
|
||
styleSheet.type = 'text/css';
|
||
styleSheet.innerHTML = styles;
|
||
document.head.appendChild(styleSheet);
|
||
}
|
||
|
||
/**
|
||
* Add styles for authentication controls
|
||
*/
|
||
addControlStyles() {
|
||
const styles = `
|
||
.insertr-auth-controls {
|
||
position: fixed;
|
||
bottom: 20px;
|
||
right: 20px;
|
||
z-index: 9999;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||
}
|
||
|
||
.insertr-auth-btn {
|
||
background: #4f46e5;
|
||
color: white;
|
||
border: none;
|
||
padding: 8px 16px;
|
||
border-radius: 6px;
|
||
font-size: 14px;
|
||
font-weight: 500;
|
||
cursor: pointer;
|
||
transition: all 0.2s;
|
||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||
}
|
||
|
||
.insertr-auth-btn:hover {
|
||
background: #4338ca;
|
||
transform: translateY(-1px);
|
||
box-shadow: 0 4px 8px rgba(0,0,0,0.15);
|
||
}
|
||
|
||
.insertr-auth-btn.insertr-authenticated {
|
||
background: #059669;
|
||
}
|
||
|
||
.insertr-auth-btn.insertr-authenticated:hover {
|
||
background: #047857;
|
||
}
|
||
|
||
.insertr-auth-btn.insertr-edit-active {
|
||
background: #dc2626;
|
||
}
|
||
|
||
.insertr-auth-btn.insertr-edit-active:hover {
|
||
background: #b91c1c;
|
||
}
|
||
|
||
.insertr-status-controls {
|
||
position: fixed;
|
||
bottom: 20px;
|
||
left: 20px;
|
||
z-index: 9999;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||
}
|
||
|
||
.insertr-status {
|
||
background: white;
|
||
border: 1px solid #e5e7eb;
|
||
border-radius: 8px;
|
||
padding: 8px 12px;
|
||
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
|
||
max-width: 200px;
|
||
}
|
||
|
||
.insertr-status-content {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
|
||
.insertr-status-text {
|
||
font-size: 12px;
|
||
font-weight: 500;
|
||
color: #374151;
|
||
}
|
||
|
||
.insertr-status-dot {
|
||
width: 8px;
|
||
height: 8px;
|
||
border-radius: 50%;
|
||
background: #9ca3af;
|
||
}
|
||
|
||
.insertr-status-dot.insertr-status-visitor {
|
||
background: #9ca3af;
|
||
}
|
||
|
||
.insertr-status-dot.insertr-status-authenticated {
|
||
background: #059669;
|
||
}
|
||
|
||
.insertr-status-dot.insertr-status-editing {
|
||
background: #dc2626;
|
||
animation: insertr-pulse 2s infinite;
|
||
}
|
||
|
||
@keyframes insertr-pulse {
|
||
0%, 100% { opacity: 1; }
|
||
50% { opacity: 0.5; }
|
||
}
|
||
|
||
/* Hide editing interface when not in edit mode */
|
||
body:not(.insertr-edit-mode) .insertr:hover::after {
|
||
display: none !important;
|
||
}
|
||
|
||
/* Only show editing features when in edit mode */
|
||
.insertr-authenticated.insertr-edit-mode .insertr {
|
||
cursor: pointer;
|
||
}
|
||
|
||
.insertr-authenticated.insertr-edit-mode .insertr:hover {
|
||
outline: 2px dashed #007cba !important;
|
||
outline-offset: 2px !important;
|
||
background-color: rgba(0, 124, 186, 0.05) !important;
|
||
}
|
||
`;
|
||
|
||
const styleSheet = document.createElement('style');
|
||
styleSheet.type = 'text/css';
|
||
styleSheet.innerHTML = styles;
|
||
document.head.appendChild(styleSheet);
|
||
}
|
||
|
||
/**
|
||
* OAuth integration placeholder
|
||
* In production, this would handle real OAuth flows
|
||
*/
|
||
async authenticateWithOAuth(provider = 'google') {
|
||
// Mock OAuth flow for now
|
||
console.log(`🔐 Mock OAuth login with ${provider}`);
|
||
|
||
// Simulate OAuth callback
|
||
setTimeout(() => {
|
||
this.state.isAuthenticated = true;
|
||
this.state.currentUser = {
|
||
name: 'OAuth User',
|
||
email: 'user@example.com',
|
||
provider: provider,
|
||
role: 'editor'
|
||
};
|
||
|
||
this.updateBodyClasses();
|
||
this.updateButtonStates();
|
||
this.updateStatusIndicator();
|
||
|
||
console.log('✅ OAuth authentication successful');
|
||
}, 1000);
|
||
}
|
||
|
||
/**
|
||
* Setup enhance button functionality
|
||
*/
|
||
setupEnhanceButton() {
|
||
const enhanceBtn = document.getElementById('insertr-enhance-btn');
|
||
if (!enhanceBtn) return;
|
||
|
||
enhanceBtn.addEventListener('click', async () => {
|
||
await this.enhanceFiles();
|
||
});
|
||
|
||
// Show enhance button only in development/authenticated mode
|
||
this.updateEnhanceButtonVisibility();
|
||
}
|
||
|
||
/**
|
||
* Update enhance button visibility based on authentication state
|
||
*/
|
||
updateEnhanceButtonVisibility() {
|
||
const enhanceBtn = document.getElementById('insertr-enhance-btn');
|
||
if (!enhanceBtn) return;
|
||
|
||
// Show enhance button when authenticated (indicates dev mode)
|
||
if (this.state.isAuthenticated) {
|
||
enhanceBtn.style.display = 'inline-block';
|
||
} else {
|
||
enhanceBtn.style.display = 'none';
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Trigger manual file enhancement
|
||
*/
|
||
async enhanceFiles() {
|
||
const enhanceBtn = document.getElementById('insertr-enhance-btn');
|
||
if (!enhanceBtn) return;
|
||
|
||
// Get site ID from window context or configuration
|
||
const siteId = window.insertrConfig?.siteId || this.options.siteId || 'demo';
|
||
|
||
try {
|
||
// Show loading state
|
||
enhanceBtn.textContent = '⏳ Enhancing...';
|
||
enhanceBtn.disabled = true;
|
||
|
||
// Smart server detection for enhance API (same logic as ApiClient)
|
||
const isDevelopment = window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1';
|
||
const enhanceUrl = isDevelopment
|
||
? `http://localhost:8080/api/enhance?site_id=${siteId}` // Development: separate API server
|
||
: `/api/enhance?site_id=${siteId}`; // Production: same-origin API
|
||
|
||
// Call enhance API
|
||
const response = await fetch(enhanceUrl, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'Authorization': `Bearer ${this.state.currentUser?.token || 'mock-token'}`
|
||
}
|
||
});
|
||
|
||
if (!response.ok) {
|
||
throw new Error(`Enhancement failed: ${response.status} ${response.statusText}`);
|
||
}
|
||
|
||
const result = await response.json();
|
||
console.log('✅ Files enhanced successfully:', result);
|
||
|
||
// Show success state briefly
|
||
enhanceBtn.textContent = '✅ Enhanced!';
|
||
|
||
// Optional: Trigger page reload to show enhanced files
|
||
setTimeout(() => {
|
||
window.location.reload();
|
||
}, 1000);
|
||
|
||
} catch (error) {
|
||
console.error('❌ Enhancement failed:', error);
|
||
enhanceBtn.textContent = '❌ Failed';
|
||
|
||
// Reset button after error
|
||
setTimeout(() => {
|
||
enhanceBtn.textContent = '🔄 Enhance';
|
||
enhanceBtn.disabled = false;
|
||
}, 2000);
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* ApiClient - Handle communication with content API
|
||
*/
|
||
class ApiClient {
|
||
constructor(options = {}) {
|
||
// Smart server detection based on environment
|
||
const isDevelopment = window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1';
|
||
const defaultEndpoint = isDevelopment
|
||
? 'http://localhost:8080/api/content' // Development: separate API server
|
||
: '/api/content'; // Production: same-origin API
|
||
|
||
this.baseUrl = options.apiEndpoint || defaultEndpoint;
|
||
this.siteId = options.siteId || 'demo';
|
||
|
||
// Log API configuration in development
|
||
if (isDevelopment && !options.apiEndpoint) {
|
||
console.log(`🔌 API Client: Using development server at ${this.baseUrl}`);
|
||
}
|
||
}
|
||
|
||
async getContent(contentId) {
|
||
try {
|
||
const response = await fetch(`${this.baseUrl}/${contentId}?site_id=${this.siteId}`);
|
||
return response.ok ? await response.json() : null;
|
||
} catch (error) {
|
||
console.warn('Failed to fetch content:', contentId, error);
|
||
return null;
|
||
}
|
||
}
|
||
|
||
|
||
async createContent(content, type, htmlMarkup) {
|
||
try {
|
||
const payload = {
|
||
html_markup: htmlMarkup, // Always send HTML markup - server extracts ID or generates new one
|
||
value: content,
|
||
type: type,
|
||
file_path: this.getCurrentFilePath() // Always include file path for consistent ID generation
|
||
};
|
||
|
||
const response = await fetch(`${this.baseUrl}?site_id=${this.siteId}`, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'Authorization': `Bearer ${this.getAuthToken()}`
|
||
},
|
||
body: JSON.stringify(payload)
|
||
});
|
||
|
||
if (response.ok) {
|
||
const result = await response.json();
|
||
console.log(`✅ Content created: ${result.id} (${result.type})`);
|
||
return result;
|
||
} else {
|
||
console.warn(`⚠️ Create failed (${response.status}): server will generate ID`);
|
||
return null;
|
||
}
|
||
} catch (error) {
|
||
if (error.name === 'TypeError' && error.message.includes('fetch')) {
|
||
console.warn(`🔌 API Server not reachable at ${this.baseUrl}`);
|
||
console.warn('💡 Start full-stack development: just dev');
|
||
} else {
|
||
console.error('Failed to create content:', error);
|
||
}
|
||
return false;
|
||
}
|
||
}
|
||
|
||
async getContentVersions(contentId) {
|
||
try {
|
||
const response = await fetch(`${this.baseUrl}/${contentId}/versions?site_id=${this.siteId}`);
|
||
if (response.ok) {
|
||
const result = await response.json();
|
||
return result.versions || [];
|
||
} else {
|
||
console.warn(`⚠️ Failed to fetch versions (${response.status}): ${contentId}`);
|
||
return [];
|
||
}
|
||
} catch (error) {
|
||
console.error('Failed to fetch version history:', contentId, error);
|
||
return [];
|
||
}
|
||
}
|
||
|
||
async rollbackContent(contentId, versionId) {
|
||
try {
|
||
const response = await fetch(`${this.baseUrl}/${contentId}/rollback?site_id=${this.siteId}`, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'Authorization': `Bearer ${this.getAuthToken()}`
|
||
},
|
||
body: JSON.stringify({
|
||
version_id: versionId
|
||
})
|
||
});
|
||
|
||
if (response.ok) {
|
||
console.log(`✅ Content rolled back: ${contentId} to version ${versionId}`);
|
||
return await response.json();
|
||
} else {
|
||
console.warn(`⚠️ Rollback failed (${response.status}): ${contentId}`);
|
||
return false;
|
||
}
|
||
} catch (error) {
|
||
console.error('Failed to rollback content:', contentId, error);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Get authentication token for API requests
|
||
* @returns {string} JWT token or mock token for development
|
||
*/
|
||
getAuthToken() {
|
||
// Check if we have a real JWT token from OAuth
|
||
const realToken = this.getStoredToken();
|
||
if (realToken && !this.isTokenExpired(realToken)) {
|
||
return realToken;
|
||
}
|
||
|
||
// Development/mock token for when no real auth is present
|
||
return this.getMockToken();
|
||
}
|
||
|
||
/**
|
||
* Get current user information from token
|
||
* @returns {string} User identifier
|
||
*/
|
||
getCurrentUser() {
|
||
const token = this.getAuthToken();
|
||
|
||
// If it's a mock token, return mock user
|
||
if (token.startsWith('mock-')) {
|
||
return 'anonymous';
|
||
}
|
||
|
||
// Parse real JWT token for user info
|
||
try {
|
||
const payload = this.parseJWT(token);
|
||
return payload.sub || payload.user_id || payload.email || 'anonymous';
|
||
} catch (error) {
|
||
console.warn('Failed to parse JWT token:', error);
|
||
return 'anonymous';
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Get stored JWT token from localStorage/sessionStorage
|
||
* @returns {string|null} Stored JWT token
|
||
*/
|
||
getStoredToken() {
|
||
// Try localStorage first (persistent), then sessionStorage (session-only)
|
||
return localStorage.getItem('insertr_auth_token') ||
|
||
sessionStorage.getItem('insertr_auth_token') ||
|
||
null;
|
||
}
|
||
|
||
/**
|
||
* Store JWT token for future requests
|
||
* @param {string} token - JWT token from OAuth provider
|
||
* @param {boolean} persistent - Whether to use localStorage (true) or sessionStorage (false)
|
||
*/
|
||
setStoredToken(token, persistent = true) {
|
||
const storage = persistent ? localStorage : sessionStorage;
|
||
storage.setItem('insertr_auth_token', token);
|
||
|
||
// Clear the other storage to avoid conflicts
|
||
const otherStorage = persistent ? sessionStorage : localStorage;
|
||
otherStorage.removeItem('insertr_auth_token');
|
||
}
|
||
|
||
/**
|
||
* Clear stored authentication token
|
||
*/
|
||
clearStoredToken() {
|
||
localStorage.removeItem('insertr_auth_token');
|
||
sessionStorage.removeItem('insertr_auth_token');
|
||
}
|
||
|
||
/**
|
||
* Generate mock JWT token for development/testing
|
||
* @returns {string} Mock JWT token
|
||
*/
|
||
getMockToken() {
|
||
// Create a mock JWT-like token for development
|
||
// Format: mock-{user}-{timestamp}-{random}
|
||
const user = 'anonymous';
|
||
const timestamp = Date.now();
|
||
const random = Math.random().toString(36).substr(2, 9);
|
||
return `mock-${user}-${timestamp}-${random}`;
|
||
}
|
||
|
||
/**
|
||
* Parse JWT token payload
|
||
* @param {string} token - JWT token
|
||
* @returns {object} Parsed payload
|
||
*/
|
||
parseJWT(token) {
|
||
if (token.startsWith('mock-')) {
|
||
// Return mock payload for development tokens
|
||
return {
|
||
sub: 'anonymous',
|
||
user_id: 'anonymous',
|
||
email: 'anonymous@localhost',
|
||
iss: 'insertr-dev',
|
||
exp: Date.now() + 24 * 60 * 60 * 1000 // 24 hours from now
|
||
};
|
||
}
|
||
|
||
try {
|
||
// Parse real JWT token
|
||
const parts = token.split('.');
|
||
if (parts.length !== 3) {
|
||
throw new Error('Invalid JWT format');
|
||
}
|
||
|
||
const payload = JSON.parse(atob(parts[1].replace(/-/g, '+').replace(/_/g, '/')));
|
||
return payload;
|
||
} catch (error) {
|
||
throw new Error(`Failed to parse JWT token: ${error.message}`);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Check if JWT token is expired
|
||
* @param {string} token - JWT token
|
||
* @returns {boolean} True if token is expired
|
||
*/
|
||
isTokenExpired(token) {
|
||
try {
|
||
const payload = this.parseJWT(token);
|
||
const now = Math.floor(Date.now() / 1000);
|
||
return payload.exp && payload.exp < now;
|
||
} catch (error) {
|
||
// If we can't parse the token, consider it expired
|
||
return true;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Initialize OAuth flow with provider (Google, GitHub, etc.)
|
||
* @param {string} provider - OAuth provider ('google', 'github', etc.)
|
||
* @returns {Promise<boolean>} Success status
|
||
*/
|
||
async initiateOAuth(provider = 'google') {
|
||
// This will be implemented when we add real OAuth integration
|
||
console.log(`🔐 OAuth flow with ${provider} not yet implemented`);
|
||
console.log('💡 For now, using mock authentication in development');
|
||
|
||
// Store a mock token for development
|
||
const mockToken = this.getMockToken();
|
||
this.setStoredToken(mockToken, true);
|
||
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* Handle OAuth callback after user returns from provider
|
||
* @param {URLSearchParams} urlParams - URL parameters from OAuth callback
|
||
* @returns {Promise<boolean>} Success status
|
||
*/
|
||
async handleOAuthCallback(urlParams) {
|
||
// This will be implemented when we add real OAuth integration
|
||
const code = urlParams.get('code');
|
||
urlParams.get('state');
|
||
|
||
if (code) {
|
||
console.log('🔐 OAuth callback received, exchanging code for token...');
|
||
// TODO: Exchange authorization code for JWT token
|
||
// const token = await this.exchangeCodeForToken(code, state);
|
||
// this.setStoredToken(token, true);
|
||
return true;
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
/**
|
||
* Get current file path from URL for consistent ID generation
|
||
* @returns {string} File path like "index.html", "about.html"
|
||
*/
|
||
getCurrentFilePath() {
|
||
const path = window.location.pathname;
|
||
if (path === '/' || path === '') {
|
||
return 'index.html';
|
||
}
|
||
// Remove leading slash: "/about.html" → "about.html"
|
||
return path.replace(/^\//, '');
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Insertr - The Tailwind of CMS
|
||
* Main library entry point
|
||
*/
|
||
|
||
|
||
// Create global Insertr instance
|
||
window.Insertr = {
|
||
// Core functionality
|
||
core: null,
|
||
editor: null,
|
||
auth: null,
|
||
apiClient: null,
|
||
|
||
// Initialize the library
|
||
init(options = {}) {
|
||
console.log('🔧 Insertr v1.0.0 initializing... (Hot Reload Ready)');
|
||
|
||
this.core = new InsertrCore(options);
|
||
this.auth = new InsertrAuth(options);
|
||
this.apiClient = new ApiClient(options);
|
||
this.editor = new InsertrEditor(this.core, this.auth, this.apiClient, options);
|
||
|
||
// Auto-initialize if DOM is ready
|
||
if (document.readyState === 'loading') {
|
||
document.addEventListener('DOMContentLoaded', () => this.start());
|
||
} else {
|
||
this.start();
|
||
}
|
||
|
||
return this;
|
||
},
|
||
|
||
// Start the system - only creates the minimal trigger
|
||
start() {
|
||
if (this.auth) {
|
||
this.auth.init(); // Creates footer trigger only
|
||
}
|
||
// Note: Editor is NOT started here, only when trigger is clicked
|
||
},
|
||
|
||
// Start the full editor system (called when trigger is activated)
|
||
startEditor() {
|
||
if (this.editor && !this.editor.isActive) {
|
||
this.editor.start();
|
||
}
|
||
},
|
||
|
||
// Public API methods
|
||
login() {
|
||
return this.auth ? this.auth.toggleAuthentication() : null;
|
||
},
|
||
|
||
logout() {
|
||
if (this.auth && this.auth.isAuthenticated()) {
|
||
this.auth.toggleAuthentication();
|
||
}
|
||
},
|
||
|
||
toggleEditMode() {
|
||
return this.auth ? this.auth.toggleEditMode() : null;
|
||
},
|
||
|
||
isAuthenticated() {
|
||
return this.auth ? this.auth.isAuthenticated() : false;
|
||
},
|
||
|
||
isEditMode() {
|
||
return this.auth ? this.auth.isEditMode() : false;
|
||
},
|
||
|
||
// TODO: Version info based on package.json?
|
||
};
|
||
|
||
// Auto-initialize in development mode with proper DOM ready handling
|
||
function autoInitialize() {
|
||
if (document.querySelector('.insertr')) {
|
||
window.Insertr.init();
|
||
}
|
||
}
|
||
|
||
// Run auto-initialization when DOM is ready
|
||
if (document.readyState === 'loading') {
|
||
document.addEventListener('DOMContentLoaded', autoInitialize);
|
||
} else {
|
||
// DOM is already ready
|
||
autoInitialize();
|
||
}
|
||
|
||
var index = window.Insertr;
|
||
|
||
return index;
|
||
|
||
})();
|