diff --git a/insertr.yaml b/insertr.yaml index ce457c4..0984c80 100644 --- a/insertr.yaml +++ b/insertr.yaml @@ -13,7 +13,7 @@ server: port: 8080 # HTTP API server port sites: # Registered sites for file-based enhancement - site_id: "demo" - path: "./demo-site" + path: "./test-sites/demo-site" domain: "localhost:3000" auto_enhance: true backup_originals: true diff --git a/internal/content/enhancer.go b/internal/content/enhancer.go index a4fe587..373bbaf 100644 --- a/internal/content/enhancer.go +++ b/internal/content/enhancer.go @@ -10,8 +10,8 @@ import ( // Enhancer combines parsing and content injection using unified engine type Enhancer struct { - engine *engine.ContentEngine - injector *Injector + engine *engine.ContentEngine + // injector functionality will be integrated into engine } // NewEnhancer creates a new HTML enhancer using unified engine @@ -26,8 +26,7 @@ func NewEnhancer(client ContentClient, siteID string) *Enhancer { } return &Enhancer{ - engine: engine.NewContentEngine(engineClient), - injector: NewInjector(client, siteID), + engine: engine.NewContentEngine(engineClient), } } diff --git a/db/postgresql/schema.sql b/internal/db/postgresql/schema.sql similarity index 100% rename from db/postgresql/schema.sql rename to internal/db/postgresql/schema.sql diff --git a/db/postgresql/setup.sql b/internal/db/postgresql/setup.sql similarity index 100% rename from db/postgresql/setup.sql rename to internal/db/postgresql/setup.sql diff --git a/db/queries/content.sql b/internal/db/queries/content.sql similarity index 100% rename from db/queries/content.sql rename to internal/db/queries/content.sql diff --git a/db/queries/versions.sql b/internal/db/queries/versions.sql similarity index 100% rename from db/queries/versions.sql rename to internal/db/queries/versions.sql diff --git a/db/sqlite/schema.sql b/internal/db/sqlite/schema.sql similarity index 100% rename from db/sqlite/schema.sql rename to internal/db/sqlite/schema.sql diff --git a/db/sqlite/setup.sql b/internal/db/sqlite/setup.sql similarity index 100% rename from db/sqlite/setup.sql rename to internal/db/sqlite/setup.sql diff --git a/internal/engine/database_client.go b/internal/engine/database_client.go index 6018632..4e99257 100644 --- a/internal/engine/database_client.go +++ b/internal/engine/database_client.go @@ -62,7 +62,7 @@ func (c *DatabaseClient) GetContent(siteID, contentID string) (*ContentItem, err } // GetBulkContent retrieves multiple content items -func (c *DatabaseClient) GetBulkContent(siteID string, contentIDs []string) ([]*ContentItem, error) { +func (c *DatabaseClient) GetBulkContent(siteID string, contentIDs []string) (map[string]ContentItem, error) { switch c.database.GetDBType() { case "sqlite3": contents, err := c.database.GetSQLiteQueries().GetBulkContent(context.Background(), sqlite.GetBulkContentParams{ @@ -73,9 +73,9 @@ func (c *DatabaseClient) GetBulkContent(siteID string, contentIDs []string) ([]* return nil, err } - items := make([]*ContentItem, len(contents)) - for i, content := range contents { - items[i] = &ContentItem{ + items := make(map[string]ContentItem) + for _, content := range contents { + items[content.ID] = ContentItem{ ID: content.ID, SiteID: content.SiteID, Value: content.Value, @@ -94,9 +94,9 @@ func (c *DatabaseClient) GetBulkContent(siteID string, contentIDs []string) ([]* return nil, err } - items := make([]*ContentItem, len(contents)) - for i, content := range contents { - items[i] = &ContentItem{ + items := make(map[string]ContentItem) + for _, content := range contents { + items[content.ID] = ContentItem{ ID: content.ID, SiteID: content.SiteID, Value: content.Value, diff --git a/internal/content/injector.go b/internal/engine/injector.go similarity index 99% rename from internal/content/injector.go rename to internal/engine/injector.go index 1f3cf86..6829eef 100644 --- a/internal/content/injector.go +++ b/internal/engine/injector.go @@ -1,4 +1,4 @@ -package content +package engine import ( "fmt" diff --git a/internal/engine/types.go b/internal/engine/types.go index 199d314..0518663 100644 --- a/internal/engine/types.go +++ b/internal/engine/types.go @@ -46,7 +46,7 @@ type ProcessedElement struct { // This will be implemented by database clients type ContentClient interface { GetContent(siteID, contentID string) (*ContentItem, error) - GetBulkContent(siteID string, contentIDs []string) ([]*ContentItem, error) + GetBulkContent(siteID string, contentIDs []string) (map[string]ContentItem, error) } // ContentItem represents a piece of content from the database diff --git a/justfile b/justfile index a49f799..ff23a0c 100644 --- a/justfile +++ b/justfile @@ -54,7 +54,7 @@ dev: build-lib build echo "" # Start demo site with prefixed output (this will block) - use local installation - cd {{justfile_directory()}} && npx --prefer-offline live-server demo-site --port=3000 --host=localhost --open=/index.html 2>&1 | sed 's/^/🌐 [DEMO] /' & + cd {{justfile_directory()}} && npx --prefer-offline live-server test-sites/demo-site --port=3000 --host=localhost --open=/index.html 2>&1 | sed 's/^/🌐 [DEMO] /' & DEMO_PID=$! # Wait for both processes @@ -70,7 +70,7 @@ dev-about: build-lib build INSERTR_DATABASE_PATH=./dev.db ./insertr serve --dev-mode & SERVER_PID=$! sleep 3 - npx --prefer-offline live-server demo-site --port=3000 --host=localhost --open=/about.html + npx --prefer-offline live-server test-sites/demo-site --port=3000 --host=localhost --open=/about.html kill $SERVER_PID 2>/dev/null || true # Check project status and validate setup @@ -208,7 +208,7 @@ help: # Enhance demo site (build-time content injection) -enhance input="demo-site" output="dist": +enhance input="test-sites/demo-site" output="dist": ./insertr enhance {{input}} --output {{output}} --mock # === Content API Server Commands === @@ -267,7 +267,7 @@ status: @echo "\nšŸ”§ Unified binary:" @ls -la insertr main.go cmd/ internal/ 2>/dev/null || echo " Missing unified binary components" @echo "\n🌐 Demo site:" - @ls -la demo-site/index.html demo-site/about.html 2>/dev/null || echo " Missing demo files" + @ls -la test-sites/demo-site/index.html test-sites/demo-site/about.html 2>/dev/null || echo " Missing demo files" @echo "" @echo "šŸš€ Development Commands:" @echo " just dev - Full-stack development (recommended)" diff --git a/lib/package.json b/lib/package.json index 88ed9c0..25dac1d 100644 --- a/lib/package.json +++ b/lib/package.json @@ -12,7 +12,7 @@ "scripts": { "build": "rollup -c && npm run copy:demo", "build:only": "rollup -c", - "copy:demo": "cp dist/insertr.js ../demo-site/insertr.js", + "copy:demo": "cp dist/insertr.js ../test-sites/demo-site/insertr.js", "watch": "rollup -c -w", "dev": "rollup -c -w" }, diff --git a/lib/rollup.config.js b/lib/rollup.config.js index e252314..6d71db0 100644 --- a/lib/rollup.config.js +++ b/lib/rollup.config.js @@ -8,10 +8,10 @@ function copyToDemo() { name: 'copy-to-demo', writeBundle() { try { - execSync('cp dist/insertr.js ../demo-site/insertr.js'); - console.log('šŸ“„ Copied to demo-site/insertr.js'); + execSync('cp dist/insertr.js ../test-sites/demo-site/insertr.js'); + console.log('šŸ“„ Copied to test-sites/demo-site/insertr.js'); } catch (error) { - console.warn('āš ļø Failed to copy to demo-site:', error.message); + console.warn('āš ļø Failed to copy to test-sites/demo-site:', error.message); } } }; diff --git a/scripts/dev.js b/scripts/dev.js index ce225b6..2d54de0 100755 --- a/scripts/dev.js +++ b/scripts/dev.js @@ -39,8 +39,8 @@ const commands = { // Check files exist const requiredFiles = [ - 'demo-site/index.html', - 'demo-site/about.html', + 'test-sites/demo-site/index.html', + 'test-sites/demo-site/about.html', 'lib/dist/insertr.js', 'lib/dist/insertr.min.js', 'cmd/serve.go', @@ -77,8 +77,8 @@ const commands = { console.log('\nšŸ“Š Project stats:'); // Count editable elements - const indexContent = fs.readFileSync('demo-site/index.html', 'utf8'); - const aboutContent = fs.readFileSync('demo-site/about.html', 'utf8'); + const indexContent = fs.readFileSync('test-sites/demo-site/index.html', 'utf8'); + const aboutContent = fs.readFileSync('test-sites/demo-site/about.html', 'utf8'); const insertrMatches = (indexContent + aboutContent).match(/class="insertr"/g) || []; console.log(` šŸ“ Editable elements: ${insertrMatches.length}`); diff --git a/sqlc.yaml b/sqlc.yaml index 4fea7a4..f45b379 100644 --- a/sqlc.yaml +++ b/sqlc.yaml @@ -3,8 +3,8 @@ sql: # SQLite configuration for development - name: "sqlite" engine: "sqlite" - queries: ["db/queries/", "db/sqlite/setup.sql"] - schema: "db/sqlite/schema.sql" + queries: ["internal/db/queries/", "internal/db/sqlite/setup.sql"] + schema: "internal/db/sqlite/schema.sql" gen: go: package: "sqlite" @@ -18,8 +18,8 @@ sql: # PostgreSQL configuration for production - name: "postgresql" engine: "postgresql" - queries: ["db/queries/", "db/postgresql/setup.sql"] - schema: "db/postgresql/schema.sql" + queries: ["internal/db/queries/", "internal/db/postgresql/setup.sql"] + schema: "internal/db/postgresql/schema.sql" gen: go: package: "postgresql" diff --git a/demo-site/README.md b/test-sites/demo-site/README.md similarity index 100% rename from demo-site/README.md rename to test-sites/demo-site/README.md diff --git a/demo-site/about.html b/test-sites/demo-site/about.html similarity index 100% rename from demo-site/about.html rename to test-sites/demo-site/about.html diff --git a/demo-site/archive/mock-api/content.json b/test-sites/demo-site/archive/mock-api/content.json similarity index 100% rename from demo-site/archive/mock-api/content.json rename to test-sites/demo-site/archive/mock-api/content.json diff --git a/demo-site/assets/style.css b/test-sites/demo-site/assets/style.css similarity index 100% rename from demo-site/assets/style.css rename to test-sites/demo-site/assets/style.css diff --git a/demo-site/index.html b/test-sites/demo-site/index.html similarity index 100% rename from demo-site/index.html rename to test-sites/demo-site/index.html diff --git a/test-sites/demo-site/insertr.js b/test-sites/demo-site/insertr.js new file mode 100644 index 0000000..d118a0a --- /dev/null +++ b/test-sites/demo-site/insertr.js @@ -0,0 +1,4127 @@ +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'); + + // Send HTML markup to server for unified ID generation + return { + contentId: existingId, // null if new content, existing ID if updating + contentType: element.getAttribute('data-content-type') || this.detectContentType(element), + element: element, + htmlMarkup: element.outerHTML // Server will generate ID from this + }; + } + + // Get current file path from URL for consistent ID generation + getCurrentFilePath() { + const path = window.location.pathname; + if (path === '/' || path === '') { + return 'index.html'; + } + // Remove leading slash: "/about.html" → "about.html" + return path.replace(/^\//, ''); + } + + // Detect content type for elements without data-content-type + detectContentType(element) { + const tag = element.tagName.toLowerCase(); + + if (element.classList.contains('insertr-group')) { + return 'markdown'; + } + + switch (tag) { + case 'h1': case 'h2': case 'h3': case 'h4': case 'h5': case 'h6': + return 'text'; + case 'p': + return 'textarea'; + case 'a': case 'button': + return 'link'; + case 'div': case 'section': + return 'markdown'; + case 'span': + return 'markdown'; // Match backend: spans support inline markdown + default: + return 'text'; + } + } + + // Get all elements with their metadata, including group elements + getAllElements() { + const directElements = document.querySelectorAll('.insertr, .insertr-group'); + const processedElements = []; + + directElements.forEach(element => { + if (element.classList.contains('insertr-group')) { + // Group element - treat as single editable unit + processedElements.push(element); + } else if (this.isContainer(element)) { + // Container element - expand to children + const children = this.findViableChildren(element); + processedElements.push(...children); + } else { + // Regular element + processedElements.push(element); + } + }); + + return Array.from(processedElements).map(el => this.getElementMetadata(el)); + } + } + + /** + * marked v16.2.1 - a markdown parser + * Copyright (c) 2011-2025, Christopher Jeffrey. (MIT Licensed) + * https://github.com/markedjs/marked + */ + + /** + * DO NOT EDIT THIS FILE + * The code in this file is generated from files in ./src/ + */ + + function L(){return {async:!1,breaks:!1,extensions:null,gfm:!0,hooks:null,pedantic:!1,renderer:null,silent:!1,tokenizer:null,walkTokens:null}}var O=L();function H(l){O=l;}var E={exec:()=>null};function h(l,e=""){let t=typeof l=="string"?l:l.source,n={replace:(r,i)=>{let s=typeof i=="string"?i:i.source;return s=s.replace(m.caret,"$1"),t=t.replace(r,s),n},getRegex:()=>new RegExp(t,e)};return n}var m={codeRemoveIndent:/^(?: {1,4}| {0,3}\t)/gm,outputLinkReplace:/\\([\[\]])/g,indentCodeCompensation:/^(\s+)(?:```)/,beginningSpace:/^\s+/,endingHash:/#$/,startingSpaceChar:/^ /,endingSpaceChar:/ $/,nonSpaceChar:/[^ ]/,newLineCharGlobal:/\n/g,tabCharGlobal:/\t/g,multipleSpaceGlobal:/\s+/g,blankLine:/^[ \t]*$/,doubleBlankLine:/\n[ \t]*\n[ \t]*$/,blockquoteStart:/^ {0,3}>/,blockquoteSetextReplace:/\n {0,3}((?:=+|-+) *)(?=\n|$)/g,blockquoteSetextReplace2:/^ {0,3}>[ \t]?/gm,listReplaceTabs:/^\t+/,listReplaceNesting:/^ {1,4}(?=( {4})*[^ ])/g,listIsTask:/^\[[ xX]\] /,listReplaceTask:/^\[[ xX]\] +/,anyLine:/\n.*\n/,hrefBrackets:/^<(.*)>$/,tableDelimiter:/[:|]/,tableAlignChars:/^\||\| *$/g,tableRowBlankLine:/\n[ \t]*$/,tableAlignRight:/^ *-+: *$/,tableAlignCenter:/^ *:-+: *$/,tableAlignLeft:/^ *:-+ *$/,startATag:/^/i,startPreScriptTag:/^<(pre|code|kbd|script)(\s|>)/i,endPreScriptTag:/^<\/(pre|code|kbd|script)(\s|>)/i,startAngleBracket:/^$/,pedanticHrefTitle:/^([^'"]*[^\s])\s+(['"])(.*)\2/,unicodeAlphaNumeric:/[\p{L}\p{N}]/u,escapeTest:/[&<>"']/,escapeReplace:/[&<>"']/g,escapeTestNoEncode:/[<>"']|&(?!(#\d{1,7}|#[Xx][a-fA-F0-9]{1,6}|\w+);)/,escapeReplaceNoEncode:/[<>"']|&(?!(#\d{1,7}|#[Xx][a-fA-F0-9]{1,6}|\w+);)/g,unescapeTest:/&(#(?:\d+)|(?:#x[0-9A-Fa-f]+)|(?:\w+));?/ig,caret:/(^|[^\[])\^/g,percentDecode:/%25/g,findPipe:/\|/g,splitPipe:/ \|/,slashPipe:/\\\|/g,carriageReturn:/\r\n|\r/g,spaceLine:/^ +$/gm,notSpaceStart:/^\S*/,endingNewline:/\n$/,listItemRegex:l=>new RegExp(`^( {0,3}${l})((?:[ ][^\\n]*)?(?:\\n|$))`),nextBulletRegex:l=>new RegExp(`^ {0,${Math.min(3,l-1)}}(?:[*+-]|\\d{1,9}[.)])((?:[ ][^\\n]*)?(?:\\n|$))`),hrRegex:l=>new RegExp(`^ {0,${Math.min(3,l-1)}}((?:- *){3,}|(?:_ *){3,}|(?:\\* *){3,})(?:\\n+|$)`),fencesBeginRegex:l=>new RegExp(`^ {0,${Math.min(3,l-1)}}(?:\`\`\`|~~~)`),headingBeginRegex:l=>new RegExp(`^ {0,${Math.min(3,l-1)}}#`),htmlBeginRegex:l=>new RegExp(`^ {0,${Math.min(3,l-1)}}<(?:[a-z].*>|!--)`,"i")},xe=/^(?:[ \t]*(?:\n|$))+/,be=/^((?: {4}| {0,3}\t)[^\n]+(?:\n(?:[ \t]*(?:\n|$))*)?)+/,Re=/^ {0,3}(`{3,}(?=[^`\n]*(?:\n|$))|~{3,})([^\n]*)(?:\n|$)(?:|([\s\S]*?)(?:\n|$))(?: {0,3}\1[~`]* *(?=\n|$)|$)/,C=/^ {0,3}((?:-[\t ]*){3,}|(?:_[ \t]*){3,}|(?:\*[ \t]*){3,})(?:\n+|$)/,Oe=/^ {0,3}(#{1,6})(?=\s|$)(.*)(?:\n+|$)/,j=/(?:[*+-]|\d{1,9}[.)])/,se=/^(?!bull |blockCode|fences|blockquote|heading|html|table)((?:.|\n(?!\s*?\n|bull |blockCode|fences|blockquote|heading|html|table))+?)\n {0,3}(=+|-+) *(?:\n+|$)/,ie=h(se).replace(/bull/g,j).replace(/blockCode/g,/(?: {4}| {0,3}\t)/).replace(/fences/g,/ {0,3}(?:`{3,}|~{3,})/).replace(/blockquote/g,/ {0,3}>/).replace(/heading/g,/ {0,3}#{1,6}/).replace(/html/g,/ {0,3}<[^\n>]+>\n/).replace(/\|table/g,"").getRegex(),Te=h(se).replace(/bull/g,j).replace(/blockCode/g,/(?: {4}| {0,3}\t)/).replace(/fences/g,/ {0,3}(?:`{3,}|~{3,})/).replace(/blockquote/g,/ {0,3}>/).replace(/heading/g,/ {0,3}#{1,6}/).replace(/html/g,/ {0,3}<[^\n>]+>\n/).replace(/table/g,/ {0,3}\|?(?:[:\- ]*\|)+[\:\- ]*\n/).getRegex(),F=/^([^\n]+(?:\n(?!hr|heading|lheading|blockquote|fences|list|html|table| +\n)[^\n]+)*)/,we=/^[^\n]+/,Q=/(?!\s*\])(?:\\[\s\S]|[^\[\]\\])+/,ye=h(/^ {0,3}\[(label)\]: *(?:\n[ \t]*)?([^<\s][^\s]*|<.*?>)(?:(?: +(?:\n[ \t]*)?| *\n[ \t]*)(title))? *(?:\n+|$)/).replace("label",Q).replace("title",/(?:"(?:\\"?|[^"\\])*"|'[^'\n]*(?:\n[^'\n]+)*\n?'|\([^()]*\))/).getRegex(),Pe=h(/^( {0,3}bull)([ \t][^\n]+?)?(?:\n|$)/).replace(/bull/g,j).getRegex(),v="address|article|aside|base|basefont|blockquote|body|caption|center|col|colgroup|dd|details|dialog|dir|div|dl|dt|fieldset|figcaption|figure|footer|form|frame|frameset|h[1-6]|head|header|hr|html|iframe|legend|li|link|main|menu|menuitem|meta|nav|noframes|ol|optgroup|option|p|param|search|section|summary|table|tbody|td|tfoot|th|thead|title|tr|track|ul",U=/|$))/,Se=h("^ {0,3}(?:<(script|pre|style|textarea)[\\s>][\\s\\S]*?(?:[^\\n]*\\n+|$)|comment[^\\n]*(\\n+|$)|<\\?[\\s\\S]*?(?:\\?>\\n*|$)|\\n*|$)|\\n*|$)|)[\\s\\S]*?(?:(?:\\n[ ]*)+\\n|$)|<(?!script|pre|style|textarea)([a-z][\\w-]*)(?:attribute)*? */?>(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n[ ]*)+\\n|$)|(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n[ ]*)+\\n|$))","i").replace("comment",U).replace("tag",v).replace("attribute",/ +[a-zA-Z:_][\w.:-]*(?: *= *"[^"\n]*"| *= *'[^'\n]*'| *= *[^\s"'=<>`]+)?/).getRegex(),oe=h(F).replace("hr",C).replace("heading"," {0,3}#{1,6}(?:\\s|$)").replace("|lheading","").replace("|table","").replace("blockquote"," {0,3}>").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html",")|<(?:script|pre|style|textarea|!--)").replace("tag",v).getRegex(),$e=h(/^( {0,3}> ?(paragraph|[^\n]*)(?:\n|$))+/).replace("paragraph",oe).getRegex(),K={blockquote:$e,code:be,def:ye,fences:Re,heading:Oe,hr:C,html:Se,lheading:ie,list:Pe,newline:xe,paragraph:oe,table:E,text:we},re=h("^ *([^\\n ].*)\\n {0,3}((?:\\| *)?:?-+:? *(?:\\| *:?-+:? *)*(?:\\| *)?)(?:\\n((?:(?! *\\n|hr|heading|blockquote|code|fences|list|html).*(?:\\n|$))*)\\n*|$)").replace("hr",C).replace("heading"," {0,3}#{1,6}(?:\\s|$)").replace("blockquote"," {0,3}>").replace("code","(?: {4}| {0,3} )[^\\n]").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html",")|<(?:script|pre|style|textarea|!--)").replace("tag",v).getRegex(),_e={...K,lheading:Te,table:re,paragraph:h(F).replace("hr",C).replace("heading"," {0,3}#{1,6}(?:\\s|$)").replace("|lheading","").replace("table",re).replace("blockquote"," {0,3}>").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html",")|<(?:script|pre|style|textarea|!--)").replace("tag",v).getRegex()},Le={...K,html:h(`^ *(?:comment *(?:\\n|\\s*$)|<(tag)[\\s\\S]+? *(?:\\n{2,}|\\s*$)|\\s]*)*?/?> *(?:\\n{2,}|\\s*$))`).replace("comment",U).replace(/tag/g,"(?!(?:a|em|strong|small|s|cite|q|dfn|abbr|data|time|code|var|samp|kbd|sub|sup|i|b|u|mark|ruby|rt|rp|bdi|bdo|span|br|wbr|ins|del|img)\\b)\\w+(?!:|[^\\w\\s@]*@)\\b").getRegex(),def:/^ *\[([^\]]+)\]: *]+)>?(?: +(["(][^\n]+[")]))? *(?:\n+|$)/,heading:/^(#{1,6})(.*)(?:\n+|$)/,fences:E,lheading:/^(.+?)\n {0,3}(=+|-+) *(?:\n+|$)/,paragraph:h(F).replace("hr",C).replace("heading",` *#{1,6} *[^ +]`).replace("lheading",ie).replace("|table","").replace("blockquote"," {0,3}>").replace("|fences","").replace("|list","").replace("|html","").replace("|tag","").getRegex()},Me=/^\\([!"#$%&'()*+,\-./:;<=>?@\[\]\\^_`{|}~])/,ze=/^(`+)([^`]|[^`][\s\S]*?[^`])\1(?!`)/,ae=/^( {2,}|\\)\n(?!\s*$)/,Ae=/^(`+|[^`])(?:(?= {2,}\n)|[\s\S]*?(?:(?=[\\]*?>/g,pe=/^(?:\*+(?:((?!\*)punct)|[^\s*]))|^_+(?:((?!_)punct)|([^\s_]))/,qe=h(pe,"u").replace(/punct/g,D).getRegex(),ve=h(pe,"u").replace(/punct/g,ue).getRegex(),ce="^[^_*]*?__[^_*]*?\\*[^_*]*?(?=__)|[^*]+(?=[^*])|(?!\\*)punct(\\*+)(?=[\\s]|$)|notPunctSpace(\\*+)(?!\\*)(?=punctSpace|$)|(?!\\*)punctSpace(\\*+)(?=notPunctSpace)|[\\s](\\*+)(?!\\*)(?=punct)|(?!\\*)punct(\\*+)(?!\\*)(?=punct)|notPunctSpace(\\*+)(?=notPunctSpace)",De=h(ce,"gu").replace(/notPunctSpace/g,le).replace(/punctSpace/g,W).replace(/punct/g,D).getRegex(),Ze=h(ce,"gu").replace(/notPunctSpace/g,Ie).replace(/punctSpace/g,Ce).replace(/punct/g,ue).getRegex(),Ge=h("^[^_*]*?\\*\\*[^_*]*?_[^_*]*?(?=\\*\\*)|[^_]+(?=[^_])|(?!_)punct(_+)(?=[\\s]|$)|notPunctSpace(_+)(?!_)(?=punctSpace|$)|(?!_)punctSpace(_+)(?=notPunctSpace)|[\\s](_+)(?!_)(?=punct)|(?!_)punct(_+)(?!_)(?=punct)","gu").replace(/notPunctSpace/g,le).replace(/punctSpace/g,W).replace(/punct/g,D).getRegex(),He=h(/\\(punct)/,"gu").replace(/punct/g,D).getRegex(),Ne=h(/^<(scheme:[^\s\x00-\x1f<>]*|email)>/).replace("scheme",/[a-zA-Z][a-zA-Z0-9+.-]{1,31}/).replace("email",/[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+(@)[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+(?![-_])/).getRegex(),je=h(U).replace("(?:-->|$)","-->").getRegex(),Fe=h("^comment|^|^<[a-zA-Z][\\w-]*(?:attribute)*?\\s*/?>|^<\\?[\\s\\S]*?\\?>|^|^").replace("comment",je).replace("attribute",/\s+[a-zA-Z:_][\w.:-]*(?:\s*=\s*"[^"]*"|\s*=\s*'[^']*'|\s*=\s*[^\s"'=<>`]+)?/).getRegex(),q=/(?:\[(?:\\[\s\S]|[^\[\]\\])*\]|\\[\s\S]|`[^`]*`|[^\[\]\\`])*?/,Qe=h(/^!?\[(label)\]\(\s*(href)(?:(?:[ \t]*(?:\n[ \t]*)?)(title))?\s*\)/).replace("label",q).replace("href",/<(?:\\.|[^\n<>\\])+>|[^ \t\n\x00-\x1f]*/).replace("title",/"(?:\\"?|[^"\\])*"|'(?:\\'?|[^'\\])*'|\((?:\\\)?|[^)\\])*\)/).getRegex(),he=h(/^!?\[(label)\]\[(ref)\]/).replace("label",q).replace("ref",Q).getRegex(),de=h(/^!?\[(ref)\](?:\[\])?/).replace("ref",Q).getRegex(),Ue=h("reflink|nolink(?!\\()","g").replace("reflink",he).replace("nolink",de).getRegex(),X={_backpedal:E,anyPunctuation:He,autolink:Ne,blockSkip:Be,br:ae,code:ze,del:E,emStrongLDelim:qe,emStrongRDelimAst:De,emStrongRDelimUnd:Ge,escape:Me,link:Qe,nolink:de,punctuation:Ee,reflink:he,reflinkSearch:Ue,tag:Fe,text:Ae,url:E},Ke={...X,link:h(/^!?\[(label)\]\((.*?)\)/).replace("label",q).getRegex(),reflink:h(/^!?\[(label)\]\s*\[([^\]]*)\]/).replace("label",q).getRegex()},N={...X,emStrongRDelimAst:Ze,emStrongLDelim:ve,url:h(/^((?:ftp|https?):\/\/|www\.)(?:[a-zA-Z0-9\-]+\.?)+[^\s<]*|^email/,"i").replace("email",/[A-Za-z0-9._+-]+(@)[a-zA-Z0-9-_]+(?:\.[a-zA-Z0-9-_]*[a-zA-Z0-9])+(?![-_])/).getRegex(),_backpedal:/(?:[^?!.,:;*_'"~()&]+|\([^)]*\)|&(?![a-zA-Z0-9]+;$)|[?!.,:;*_'"~)]+(?!$))+/,del:/^(~~?)(?=[^\s~])((?:\\[\s\S]|[^\\])*?(?:\\[\s\S]|[^\s~\\]))\1(?=[^~]|$)/,text:/^([`~]+|[^`~])(?:(?= {2,}\n)|(?=[a-zA-Z0-9.!#$%&'*+\/=?_`{\|}~-]+@)|[\s\S]*?(?:(?=[\\":">",'"':""","'":"'"},ke=l=>Xe[l];function w(l,e){if(e){if(m.escapeTest.test(l))return l.replace(m.escapeReplace,ke)}else if(m.escapeTestNoEncode.test(l))return l.replace(m.escapeReplaceNoEncode,ke);return l}function J(l){try{l=encodeURI(l).replace(m.percentDecode,"%");}catch{return null}return l}function V(l,e){let t=l.replace(m.findPipe,(i,s,o)=>{let a=!1,u=s;for(;--u>=0&&o[u]==="\\";)a=!a;return a?"|":" |"}),n=t.split(m.splitPipe),r=0;if(n[0].trim()||n.shift(),n.length>0&&!n.at(-1)?.trim()&&n.pop(),e)if(n.length>e)n.splice(e);else for(;n.length0?-2:-1}function fe(l,e,t,n,r){let i=e.href,s=e.title||null,o=l[1].replace(r.other.outputLinkReplace,"$1");n.state.inLink=!0;let a={type:l[0].charAt(0)==="!"?"image":"link",raw:t,href:i,title:s,text:o,tokens:n.inlineTokens(o)};return n.state.inLink=!1,a}function Je(l,e,t){let n=l.match(t.other.indentCodeCompensation);if(n===null)return e;let r=n[1];return e.split(` +`).map(i=>{let s=i.match(t.other.beginningSpace);if(s===null)return i;let[o]=s;return o.length>=r.length?i.slice(r.length):i}).join(` +`)}var y=class{options;rules;lexer;constructor(e){this.options=e||O;}space(e){let t=this.rules.block.newline.exec(e);if(t&&t[0].length>0)return {type:"space",raw:t[0]}}code(e){let t=this.rules.block.code.exec(e);if(t){let n=t[0].replace(this.rules.other.codeRemoveIndent,"");return {type:"code",raw:t[0],codeBlockStyle:"indented",text:this.options.pedantic?n:z(n,` +`)}}}fences(e){let t=this.rules.block.fences.exec(e);if(t){let n=t[0],r=Je(n,t[3]||"",this.rules);return {type:"code",raw:n,lang:t[2]?t[2].trim().replace(this.rules.inline.anyPunctuation,"$1"):t[2],text:r}}}heading(e){let t=this.rules.block.heading.exec(e);if(t){let n=t[2].trim();if(this.rules.other.endingHash.test(n)){let r=z(n,"#");(this.options.pedantic||!r||this.rules.other.endingSpaceChar.test(r))&&(n=r.trim());}return {type:"heading",raw:t[0],depth:t[1].length,text:n,tokens:this.lexer.inline(n)}}}hr(e){let t=this.rules.block.hr.exec(e);if(t)return {type:"hr",raw:z(t[0],` +`)}}blockquote(e){let t=this.rules.block.blockquote.exec(e);if(t){let n=z(t[0],` +`).split(` +`),r="",i="",s=[];for(;n.length>0;){let o=!1,a=[],u;for(u=0;u1,i={type:"list",raw:"",ordered:r,start:r?+n.slice(0,-1):"",loose:!1,items:[]};n=r?`\\d{1,9}\\${n.slice(-1)}`:`\\${n}`,this.options.pedantic&&(n=r?n:"[*+-]");let s=this.rules.other.listItemRegex(n),o=!1;for(;e;){let u=!1,p="",c="";if(!(t=s.exec(e))||this.rules.block.hr.test(e))break;p=t[0],e=e.substring(p.length);let f=t[2].split(` +`,1)[0].replace(this.rules.other.listReplaceTabs,Z=>" ".repeat(3*Z.length)),k=e.split(` +`,1)[0],x=!f.trim(),g=0;if(this.options.pedantic?(g=2,c=f.trimStart()):x?g=t[1].length+1:(g=t[2].search(this.rules.other.nonSpaceChar),g=g>4?1:g,c=f.slice(g),g+=t[1].length),x&&this.rules.other.blankLine.test(k)&&(p+=k+` +`,e=e.substring(k.length+1),u=!0),!u){let Z=this.rules.other.nextBulletRegex(g),ee=this.rules.other.hrRegex(g),te=this.rules.other.fencesBeginRegex(g),ne=this.rules.other.headingBeginRegex(g),me=this.rules.other.htmlBeginRegex(g);for(;e;){let G=e.split(` +`,1)[0],A;if(k=G,this.options.pedantic?(k=k.replace(this.rules.other.listReplaceNesting," "),A=k):A=k.replace(this.rules.other.tabCharGlobal," "),te.test(k)||ne.test(k)||me.test(k)||Z.test(k)||ee.test(k))break;if(A.search(this.rules.other.nonSpaceChar)>=g||!k.trim())c+=` +`+A.slice(g);else {if(x||f.replace(this.rules.other.tabCharGlobal," ").search(this.rules.other.nonSpaceChar)>=4||te.test(f)||ne.test(f)||ee.test(f))break;c+=` +`+k;}!x&&!k.trim()&&(x=!0),p+=G+` +`,e=e.substring(G.length+1),f=A.slice(g);}}i.loose||(o?i.loose=!0:this.rules.other.doubleBlankLine.test(p)&&(o=!0));let T=null,Y;this.options.gfm&&(T=this.rules.other.listIsTask.exec(c),T&&(Y=T[0]!=="[ ] ",c=c.replace(this.rules.other.listReplaceTask,""))),i.items.push({type:"list_item",raw:p,task:!!T,checked:Y,loose:!1,text:c,tokens:[]}),i.raw+=p;}let a=i.items.at(-1);if(a)a.raw=a.raw.trimEnd(),a.text=a.text.trimEnd();else return;i.raw=i.raw.trimEnd();for(let u=0;uf.type==="space"),c=p.length>0&&p.some(f=>this.rules.other.anyLine.test(f.raw));i.loose=c;}if(i.loose)for(let u=0;u({text:a,tokens:this.lexer.inline(a),header:!1,align:s.align[u]})));return s}}lheading(e){let t=this.rules.block.lheading.exec(e);if(t)return {type:"heading",raw:t[0],depth:t[2].charAt(0)==="="?1:2,text:t[1],tokens:this.lexer.inline(t[1])}}paragraph(e){let t=this.rules.block.paragraph.exec(e);if(t){let n=t[1].charAt(t[1].length-1)===` +`?t[1].slice(0,-1):t[1];return {type:"paragraph",raw:t[0],text:n,tokens:this.lexer.inline(n)}}}text(e){let t=this.rules.block.text.exec(e);if(t)return {type:"text",raw:t[0],text:t[0],tokens:this.lexer.inline(t[0])}}escape(e){let t=this.rules.inline.escape.exec(e);if(t)return {type:"escape",raw:t[0],text:t[1]}}tag(e){let t=this.rules.inline.tag.exec(e);if(t)return !this.lexer.state.inLink&&this.rules.other.startATag.test(t[0])?this.lexer.state.inLink=!0:this.lexer.state.inLink&&this.rules.other.endATag.test(t[0])&&(this.lexer.state.inLink=!1),!this.lexer.state.inRawBlock&&this.rules.other.startPreScriptTag.test(t[0])?this.lexer.state.inRawBlock=!0:this.lexer.state.inRawBlock&&this.rules.other.endPreScriptTag.test(t[0])&&(this.lexer.state.inRawBlock=!1),{type:"html",raw:t[0],inLink:this.lexer.state.inLink,inRawBlock:this.lexer.state.inRawBlock,block:!1,text:t[0]}}link(e){let t=this.rules.inline.link.exec(e);if(t){let n=t[2].trim();if(!this.options.pedantic&&this.rules.other.startAngleBracket.test(n)){if(!this.rules.other.endAngleBracket.test(n))return;let s=z(n.slice(0,-1),"\\");if((n.length-s.length)%2===0)return}else {let s=ge(t[2],"()");if(s===-2)return;if(s>-1){let a=(t[0].indexOf("!")===0?5:4)+t[1].length+s;t[2]=t[2].substring(0,s),t[0]=t[0].substring(0,a).trim(),t[3]="";}}let r=t[2],i="";if(this.options.pedantic){let s=this.rules.other.pedanticHrefTitle.exec(r);s&&(r=s[1],i=s[3]);}else i=t[3]?t[3].slice(1,-1):"";return r=r.trim(),this.rules.other.startAngleBracket.test(r)&&(this.options.pedantic&&!this.rules.other.endAngleBracket.test(n)?r=r.slice(1):r=r.slice(1,-1)),fe(t,{href:r&&r.replace(this.rules.inline.anyPunctuation,"$1"),title:i&&i.replace(this.rules.inline.anyPunctuation,"$1")},t[0],this.lexer,this.rules)}}reflink(e,t){let n;if((n=this.rules.inline.reflink.exec(e))||(n=this.rules.inline.nolink.exec(e))){let r=(n[2]||n[1]).replace(this.rules.other.multipleSpaceGlobal," "),i=t[r.toLowerCase()];if(!i){let s=n[0].charAt(0);return {type:"text",raw:s,text:s}}return fe(n,i,n[0],this.lexer,this.rules)}}emStrong(e,t,n=""){let r=this.rules.inline.emStrongLDelim.exec(e);if(!r||r[3]&&n.match(this.rules.other.unicodeAlphaNumeric))return;if(!(r[1]||r[2]||"")||!n||this.rules.inline.punctuation.exec(n)){let s=[...r[0]].length-1,o,a,u=s,p=0,c=r[0][0]==="*"?this.rules.inline.emStrongRDelimAst:this.rules.inline.emStrongRDelimUnd;for(c.lastIndex=0,t=t.slice(-1*e.length+s);(r=c.exec(t))!=null;){if(o=r[1]||r[2]||r[3]||r[4]||r[5]||r[6],!o)continue;if(a=[...o].length,r[3]||r[4]){u+=a;continue}else if((r[5]||r[6])&&s%3&&!((s+a)%3)){p+=a;continue}if(u-=a,u>0)continue;a=Math.min(a,a+u+p);let f=[...r[0]][0].length,k=e.slice(0,s+r.index+f+a);if(Math.min(s,a)%2){let g=k.slice(1,-1);return {type:"em",raw:k,text:g,tokens:this.lexer.inlineTokens(g)}}let x=k.slice(2,-2);return {type:"strong",raw:k,text:x,tokens:this.lexer.inlineTokens(x)}}}}codespan(e){let t=this.rules.inline.code.exec(e);if(t){let n=t[2].replace(this.rules.other.newLineCharGlobal," "),r=this.rules.other.nonSpaceChar.test(n),i=this.rules.other.startingSpaceChar.test(n)&&this.rules.other.endingSpaceChar.test(n);return r&&i&&(n=n.substring(1,n.length-1)),{type:"codespan",raw:t[0],text:n}}}br(e){let t=this.rules.inline.br.exec(e);if(t)return {type:"br",raw:t[0]}}del(e){let t=this.rules.inline.del.exec(e);if(t)return {type:"del",raw:t[0],text:t[2],tokens:this.lexer.inlineTokens(t[2])}}autolink(e){let t=this.rules.inline.autolink.exec(e);if(t){let n,r;return t[2]==="@"?(n=t[1],r="mailto:"+n):(n=t[1],r=n),{type:"link",raw:t[0],text:n,href:r,tokens:[{type:"text",raw:n,text:n}]}}}url(e){let t;if(t=this.rules.inline.url.exec(e)){let n,r;if(t[2]==="@")n=t[0],r="mailto:"+n;else {let i;do i=t[0],t[0]=this.rules.inline._backpedal.exec(t[0])?.[0]??"";while(i!==t[0]);n=t[0],t[1]==="www."?r="http://"+t[0]:r=t[0];}return {type:"link",raw:t[0],text:n,href:r,tokens:[{type:"text",raw:n,text:n}]}}}inlineText(e){let t=this.rules.inline.text.exec(e);if(t){let n=this.lexer.state.inRawBlock;return {type:"text",raw:t[0],text:t[0],escaped:n}}}};var b=class l{tokens;options;state;tokenizer;inlineQueue;constructor(e){this.tokens=[],this.tokens.links=Object.create(null),this.options=e||O,this.options.tokenizer=this.options.tokenizer||new y,this.tokenizer=this.options.tokenizer,this.tokenizer.options=this.options,this.tokenizer.lexer=this,this.inlineQueue=[],this.state={inLink:!1,inRawBlock:!1,top:!0};let t={other:m,block:I.normal,inline:M.normal};this.options.pedantic?(t.block=I.pedantic,t.inline=M.pedantic):this.options.gfm&&(t.block=I.gfm,this.options.breaks?t.inline=M.breaks:t.inline=M.gfm),this.tokenizer.rules=t;}static get rules(){return {block:I,inline:M}}static lex(e,t){return new l(t).lex(e)}static lexInline(e,t){return new l(t).inlineTokens(e)}lex(e){e=e.replace(m.carriageReturn,` +`),this.blockTokens(e,this.tokens);for(let t=0;t(r=s.call({lexer:this},e,t))?(e=e.substring(r.raw.length),t.push(r),!0):!1))continue;if(r=this.tokenizer.space(e)){e=e.substring(r.raw.length);let s=t.at(-1);r.raw.length===1&&s!==void 0?s.raw+=` +`:t.push(r);continue}if(r=this.tokenizer.code(e)){e=e.substring(r.raw.length);let s=t.at(-1);s?.type==="paragraph"||s?.type==="text"?(s.raw+=(s.raw.endsWith(` +`)?"":` +`)+r.raw,s.text+=` +`+r.text,this.inlineQueue.at(-1).src=s.text):t.push(r);continue}if(r=this.tokenizer.fences(e)){e=e.substring(r.raw.length),t.push(r);continue}if(r=this.tokenizer.heading(e)){e=e.substring(r.raw.length),t.push(r);continue}if(r=this.tokenizer.hr(e)){e=e.substring(r.raw.length),t.push(r);continue}if(r=this.tokenizer.blockquote(e)){e=e.substring(r.raw.length),t.push(r);continue}if(r=this.tokenizer.list(e)){e=e.substring(r.raw.length),t.push(r);continue}if(r=this.tokenizer.html(e)){e=e.substring(r.raw.length),t.push(r);continue}if(r=this.tokenizer.def(e)){e=e.substring(r.raw.length);let s=t.at(-1);s?.type==="paragraph"||s?.type==="text"?(s.raw+=(s.raw.endsWith(` +`)?"":` +`)+r.raw,s.text+=` +`+r.raw,this.inlineQueue.at(-1).src=s.text):this.tokens.links[r.tag]||(this.tokens.links[r.tag]={href:r.href,title:r.title},t.push(r));continue}if(r=this.tokenizer.table(e)){e=e.substring(r.raw.length),t.push(r);continue}if(r=this.tokenizer.lheading(e)){e=e.substring(r.raw.length),t.push(r);continue}let i=e;if(this.options.extensions?.startBlock){let s=1/0,o=e.slice(1),a;this.options.extensions.startBlock.forEach(u=>{a=u.call({lexer:this},o),typeof a=="number"&&a>=0&&(s=Math.min(s,a));}),s<1/0&&s>=0&&(i=e.substring(0,s+1));}if(this.state.top&&(r=this.tokenizer.paragraph(i))){let s=t.at(-1);n&&s?.type==="paragraph"?(s.raw+=(s.raw.endsWith(` +`)?"":` +`)+r.raw,s.text+=` +`+r.text,this.inlineQueue.pop(),this.inlineQueue.at(-1).src=s.text):t.push(r),n=i.length!==e.length,e=e.substring(r.raw.length);continue}if(r=this.tokenizer.text(e)){e=e.substring(r.raw.length);let s=t.at(-1);s?.type==="text"?(s.raw+=(s.raw.endsWith(` +`)?"":` +`)+r.raw,s.text+=` +`+r.text,this.inlineQueue.pop(),this.inlineQueue.at(-1).src=s.text):t.push(r);continue}if(e){let s="Infinite loop on byte: "+e.charCodeAt(0);if(this.options.silent){console.error(s);break}else throw new Error(s)}}return this.state.top=!0,t}inline(e,t=[]){return this.inlineQueue.push({src:e,tokens:t}),t}inlineTokens(e,t=[]){let n=e,r=null;if(this.tokens.links){let o=Object.keys(this.tokens.links);if(o.length>0)for(;(r=this.tokenizer.rules.inline.reflinkSearch.exec(n))!=null;)o.includes(r[0].slice(r[0].lastIndexOf("[")+1,-1))&&(n=n.slice(0,r.index)+"["+"a".repeat(r[0].length-2)+"]"+n.slice(this.tokenizer.rules.inline.reflinkSearch.lastIndex));}for(;(r=this.tokenizer.rules.inline.anyPunctuation.exec(n))!=null;)n=n.slice(0,r.index)+"++"+n.slice(this.tokenizer.rules.inline.anyPunctuation.lastIndex);for(;(r=this.tokenizer.rules.inline.blockSkip.exec(n))!=null;)n=n.slice(0,r.index)+"["+"a".repeat(r[0].length-2)+"]"+n.slice(this.tokenizer.rules.inline.blockSkip.lastIndex);let i=!1,s="";for(;e;){i||(s=""),i=!1;let o;if(this.options.extensions?.inline?.some(u=>(o=u.call({lexer:this},e,t))?(e=e.substring(o.raw.length),t.push(o),!0):!1))continue;if(o=this.tokenizer.escape(e)){e=e.substring(o.raw.length),t.push(o);continue}if(o=this.tokenizer.tag(e)){e=e.substring(o.raw.length),t.push(o);continue}if(o=this.tokenizer.link(e)){e=e.substring(o.raw.length),t.push(o);continue}if(o=this.tokenizer.reflink(e,this.tokens.links)){e=e.substring(o.raw.length);let u=t.at(-1);o.type==="text"&&u?.type==="text"?(u.raw+=o.raw,u.text+=o.text):t.push(o);continue}if(o=this.tokenizer.emStrong(e,n,s)){e=e.substring(o.raw.length),t.push(o);continue}if(o=this.tokenizer.codespan(e)){e=e.substring(o.raw.length),t.push(o);continue}if(o=this.tokenizer.br(e)){e=e.substring(o.raw.length),t.push(o);continue}if(o=this.tokenizer.del(e)){e=e.substring(o.raw.length),t.push(o);continue}if(o=this.tokenizer.autolink(e)){e=e.substring(o.raw.length),t.push(o);continue}if(!this.state.inLink&&(o=this.tokenizer.url(e))){e=e.substring(o.raw.length),t.push(o);continue}let a=e;if(this.options.extensions?.startInline){let u=1/0,p=e.slice(1),c;this.options.extensions.startInline.forEach(f=>{c=f.call({lexer:this},p),typeof c=="number"&&c>=0&&(u=Math.min(u,c));}),u<1/0&&u>=0&&(a=e.substring(0,u+1));}if(o=this.tokenizer.inlineText(a)){e=e.substring(o.raw.length),o.raw.slice(-1)!=="_"&&(s=o.raw.slice(-1)),i=!0;let u=t.at(-1);u?.type==="text"?(u.raw+=o.raw,u.text+=o.text):t.push(o);continue}if(e){let u="Infinite loop on byte: "+e.charCodeAt(0);if(this.options.silent){console.error(u);break}else throw new Error(u)}}return t}};var P=class{options;parser;constructor(e){this.options=e||O;}space(e){return ""}code({text:e,lang:t,escaped:n}){let r=(t||"").match(m.notSpaceStart)?.[0],i=e.replace(m.endingNewline,"")+` +`;return r?'
'+(n?i:w(i,!0))+`
+`:"
"+(n?i:w(i,!0))+`
+`}blockquote({tokens:e}){return `
+${this.parser.parse(e)}
+`}html({text:e}){return e}def(e){return ""}heading({tokens:e,depth:t}){return `${this.parser.parseInline(e)} +`}hr(e){return `
+`}list(e){let t=e.ordered,n=e.start,r="";for(let o=0;o +`+r+" +`}listitem(e){let t="";if(e.task){let n=this.checkbox({checked:!!e.checked});e.loose?e.tokens[0]?.type==="paragraph"?(e.tokens[0].text=n+" "+e.tokens[0].text,e.tokens[0].tokens&&e.tokens[0].tokens.length>0&&e.tokens[0].tokens[0].type==="text"&&(e.tokens[0].tokens[0].text=n+" "+w(e.tokens[0].tokens[0].text),e.tokens[0].tokens[0].escaped=!0)):e.tokens.unshift({type:"text",raw:n+" ",text:n+" ",escaped:!0}):t+=n+" ";}return t+=this.parser.parse(e.tokens,!!e.loose),`
  • ${t}
  • +`}checkbox({checked:e}){return "'}paragraph({tokens:e}){return `

    ${this.parser.parseInline(e)}

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

    An error occurred:

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

    ${p.trim()}

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

    ${html}

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

    Version History

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