// Sketchnote Scaffolding Generator // Detects existing canvas content and positions structured info around it. // Works with sermon notes, book visuals, or any Excalidraw hybrid note. // Parses "# Back of Card" to determine note type and adapt layout. // // Open any hybrid note in Excalidraw view, run from Cmd+P. if (!ea.verifyMinimumPluginVersion || !ea.verifyMinimumPluginVersion("2.0.0")) { new Notice("This script requires Excalidraw plugin v2.0+"); return; } var view = ea.targetView; if (!view || !view.file) { new Notice("Open a note in Excalidraw view first."); return; } var fileContent = await app.vault.read(view.file); var backOfCardMatch = fileContent.match( /# Back of Card\s*\n([\s\S]*?)(?=\n# Excalidraw Data|\n# Text Elements|$)/ ); if (!backOfCardMatch) { new Notice("No '# Back of Card' section found."); return; } var markdown = backOfCardMatch[1]; // --- PARSING HELPERS --- function extractSection(md, heading, level) { level = level || 2; var prefix = ""; for (var i = 0; i < level; i++) prefix += "#"; var re = new RegExp( prefix + "\\s+" + heading + "\\s*\\n([\\s\\S]*?)(?=\\n#{1," + level + "}\\s|$)" ); var m = md.match(re); return m ? m[1].trim() : ""; } function extractBullets(text) { var results = {}; var re = /\*\*(.+?):\*\*\s*(.+)/g; var m; while ((m = re.exec(text)) !== null) { results[m[1].trim()] = m[2].trim(); } return results; } function extractH2Headings(md) { var headings = []; var re = /^## (.+)/gm; var m; while ((m = re.exec(md)) !== null) { headings.push(m[1].trim()); } return headings; } function firstLine(text) { if (!text) return ""; var lines = text.split("\n"); for (var i = 0; i < lines.length; i++) { var l = lines[i].trim(); if (l && !l.startsWith("#")) return l.substring(0, 80); } return ""; } // --- DETECT NOTE TYPE --- var h2s = extractH2Headings(markdown); var h2Lower = h2s.join("|").toLowerCase(); var noteType = "generic"; if (h2Lower.indexOf("liturgical context") >= 0 || h2Lower.indexOf("order of worship") >= 0) { noteType = "sermon"; } else if (h2Lower.indexOf("central thesis") >= 0 || h2Lower.indexOf("book details") >= 0) { noteType = "book"; } // --- COLOR SYSTEM --- var BLUE = "#1971c2"; var GREEN = "#2f9e44"; var ORANGE = "#e8590c"; var PURPLE = "#7048e8"; var GRAY = "#868e96"; function lightHex(hex) { var map = { "#7048e8": "#e5dbff", "#1971c2": "#d0ebff", "#2f9e44": "#d3f9d8", "#f59f00": "#fff3bf", "#e03131": "#ffe3e3", "#d6336c": "#ffdeeb", "#868e96": "#f1f3f5", "#e8590c": "#ffe8cc", }; return map[hex] || "#f1f3f5"; } function getLiturgicalHex(colorName) { var lower = (colorName || "").toLowerCase(); if (lower.indexOf("purple") >= 0 || lower.indexOf("violet") >= 0) return "#7048e8"; if (lower.indexOf("blue") >= 0) return "#1971c2"; if (lower.indexOf("green") >= 0) return "#2f9e44"; if (lower.indexOf("white") >= 0 || lower.indexOf("gold") >= 0) return "#f59f00"; if (lower.indexOf("red") >= 0) return "#e03131"; if (lower.indexOf("rose") >= 0 || lower.indexOf("pink") >= 0) return "#d6336c"; return "#868e96"; } // --- DETECT EXISTING CONTENT --- var existingEls = ea.getViewElements(); var centerX = 0; var centerY = 0; var centerW = 1200; var centerH = 800; if (existingEls.length > 0) { var minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity; for (var e = 0; e < existingEls.length; e++) { var el = existingEls[e]; var ex = el.x || 0; var ey = el.y || 0; var ew = el.width || 0; var eh = el.height || 0; if (ex < minX) minX = ex; if (ey < minY) minY = ey; if (ex + ew > maxX) maxX = ex + ew; if (ey + eh > maxY) maxY = ey + eh; } centerX = minX; centerY = minY; centerW = maxX - minX; centerH = maxY - minY; } var GAP = 40; var TOP = centerY - GAP; var BOTTOM = centerY + centerH + GAP; var LEFT = centerX - GAP; var RIGHT = centerX + centerW + GAP; // --- COMMON STYLE SETUP --- ea.style.roughness = 1; ea.style.strokeWidth = 2; ea.style.fontFamily = 1; ea.style.strokeColor = "#1e1e1e"; // ============================================================ // SERMON NOTE SCAFFOLDING // ============================================================ if (noteType === "sermon") { var liturgicalSection = extractSection(markdown, "Liturgical Context"); var christianCal = extractSection(liturgicalSection, "Christian Calendar", 3); var calValues = extractBullets(christianCal); var season = calValues["Season"] || "Unknown"; var week = calValues["Week"] || ""; var litYear = calValues["Liturgical Year"] || ""; var litColor = calValues["Liturgical Color"] || ""; var serviceSection = extractSection(markdown, "Service Information"); var serviceValues = extractBullets(serviceSection); var church = serviceValues["Church"] || ""; var serviceDate = serviceValues["Date"] || ""; var pastor = serviceValues["Pastor"] || ""; var rclSection = extractSection(liturgicalSection, "Revised Common Lectionary Readings", 3); var rclValues = extractBullets(rclSection); var nlSection = extractSection(liturgicalSection, "Narrative Lectionary Reading", 3); var nlValues = extractBullets(nlSection); var orderSection = extractSection(markdown, "Order of Worship"); var sermonMatch = orderSection.match( /####\s+Sermon[:\s]*["\u201C]?([^"\u201D\n]+)["\u201D]?\s*(?:\u2014|--|[-])\s*(.+)/ ); var sermonTitle = sermonMatch ? sermonMatch[1].trim() : ""; if (!sermonTitle) { var altMatch = orderSection.match(/####\s+Sermon[:\s]*(.+)/); sermonTitle = altMatch ? altMatch[1].trim().replace(/^["'\u201C]+|["'\u201D]+$/g, "") : "Sermon"; } var preacher = sermonMatch ? sermonMatch[2].trim() : pastor; var sermonTextMatch = orderSection.match(/\*\*Scripture Text:\*\*\s*(.+)/); var sermonScripture = sermonTextMatch ? sermonTextMatch[1].trim() : ""; var hymnRegex = /####\s+(?:Opening Hymn|Hymn of Response|Closing Hymn|Hymn)[^:]*:\s*(.+)/g; var hymns = []; var hm; while ((hm = hymnRegex.exec(orderSection)) !== null) { hymns.push(hm[1].trim()); } var seasonHex = getLiturgicalHex(litColor); var seasonBg = lightHex(seasonHex); // TOP-LEFT: Liturgical Badge ea.style.backgroundColor = seasonBg; ea.style.fontSize = 16; var seasonLabel = season; if (week) seasonLabel += "\n" + week; if (litYear) seasonLabel += "\nYear " + litYear; ea.addText(LEFT, TOP - 120, seasonLabel, { width: 220, textAlign: "center", box: "blob", boxPadding: 12, boxStrokeColor: seasonHex, }); if (litColor) { ea.style.backgroundColor = seasonHex; ea.style.fontSize = 12; ea.addText(LEFT + 30, TOP - 30, litColor, { width: 160, textAlign: "center", box: "ellipse", boxPadding: 6, boxStrokeColor: seasonHex, }); } // TOP-CENTER: Sermon Title + Date ea.style.backgroundColor = lightHex(GREEN); ea.style.fontSize = 22; var titleLabel = sermonTitle || "Sermon"; if (serviceDate) titleLabel = serviceDate + "\n" + titleLabel; ea.addText(LEFT + 260, TOP - 110, titleLabel, { width: 500, textAlign: "center", box: "box", boxPadding: 14, boxStrokeColor: GREEN, }); if (preacher) { ea.style.backgroundColor = "transparent"; ea.style.fontSize = 13; ea.addText(LEFT + 410, TOP - 30, "\u2014 " + preacher, { width: 200, textAlign: "center" }); } // TOP-RIGHT: Scripture Text if (sermonScripture) { ea.style.backgroundColor = lightHex(BLUE); ea.style.fontSize = 14; ea.addText(RIGHT - 280, TOP - 100, sermonScripture, { width: 260, textAlign: "center", box: "diamond", boxPadding: 18, boxStrokeColor: BLUE, }); } // LEFT SIDE: RCL Readings var rclKeys = ["Old Testament", "Psalm", "Epistle", "Gospel"]; var readY = centerY + 20; for (var i = 0; i < rclKeys.length; i++) { var key = rclKeys[i]; var val = rclValues[key]; if (val) { ea.style.backgroundColor = lightHex(BLUE); ea.style.fontSize = 11; ea.addText(LEFT - 200, readY, key + ":\n" + val.replace(/\s*\u2014.*$/, "").trim(), { width: 180, textAlign: "center", box: "box", boxPadding: 8, boxStrokeColor: BLUE, }); readY += 70; } } if (nlValues["Reading"]) { ea.style.backgroundColor = lightHex(PURPLE); ea.style.fontSize = 11; ea.addText(LEFT - 200, readY, "NL:\n" + nlValues["Reading"].replace(/\s*\u2014.*$/, ""), { width: 180, textAlign: "center", box: "box", boxPadding: 8, boxStrokeColor: PURPLE, }); } // RIGHT SIDE: Reflection Zones ea.style.fontSize = 13; ea.style.backgroundColor = lightHex(ORANGE); ea.addText(RIGHT + 20, centerY, "What Spoke to Me\n\n\n", { width: 240, height: 120, textAlign: "left", textVerticalAlign: "top", box: "box", boxPadding: 10, boxStrokeColor: ORANGE, }); ea.style.backgroundColor = lightHex(PURPLE); ea.addText(RIGHT + 20, centerY + 160, "Connections\n\n\n", { width: 240, height: 120, textAlign: "left", textVerticalAlign: "top", box: "box", boxPadding: 10, boxStrokeColor: PURPLE, }); ea.style.backgroundColor = lightHex(ORANGE); ea.addText(RIGHT + 20, centerY + 320, "Action Items\n\n\n", { width: 240, height: 120, textAlign: "left", textVerticalAlign: "top", box: "box", boxPadding: 10, boxStrokeColor: ORANGE, }); // BOTTOM: Hymns if (hymns.length > 0) { ea.style.fontSize = 11; var hymnX = LEFT; for (var h = 0; h < hymns.length; h++) { ea.style.backgroundColor = lightHex(GRAY); var hw = Math.min(350, Math.max(160, hymns[h].length * 7)); ea.addText(hymnX, BOTTOM + 20, hymns[h], { width: hw, textAlign: "center", box: "box", boxPadding: 8, boxStrokeColor: GRAY, }); hymnX += hw + 15; } } // BOTTOM-RIGHT: Church info var infoText = church || ""; if (pastor) infoText += "\n" + pastor; if (infoText) { ea.style.backgroundColor = lightHex(GRAY); ea.style.fontSize = 10; ea.addText(RIGHT - 200, BOTTOM + 20, infoText, { width: 200, textAlign: "right", box: "box", boxPadding: 6, boxStrokeColor: GRAY, }); } // ============================================================ // BOOK NOTE SCAFFOLDING // ============================================================ } else if (noteType === "book") { var bookDetails = extractSection(markdown, "Book Details"); var bookValues = extractBullets(bookDetails); var bookTitle = bookValues["Title"] || view.file.basename.replace(/ Visual$/, ""); var bookAuthor = bookValues["Author"] || ""; var thesis = extractSection(markdown, "Central Thesis \\(expanded\\)"); if (!thesis) thesis = extractSection(markdown, "Central Thesis"); var thesisSummary = firstLine(thesis); var conceptsSection = extractSection(markdown, "Key Concepts \\(detailed\\)"); if (!conceptsSection) conceptsSection = extractSection(markdown, "Key Concepts"); var conceptHeadings = []; var conceptRe = /^### (.+)/gm; var cm; while ((cm = conceptRe.exec(conceptsSection)) !== null) { conceptHeadings.push(cm[1].trim()); } var reflectionSection = extractSection(markdown, "Personal Reflection"); var surprisedText = firstLine(extractSection(reflectionSection, "What surprised me\\?", 3)); var challengedText = firstLine(extractSection(reflectionSection, "What challenged", 3)); var actionSection = extractSection(markdown, "Action Items"); var actions = []; var actionRe = /- \[.\]\s*(.+)/g; var am; while ((am = actionRe.exec(actionSection)) !== null) { actions.push(am[1].trim()); } var ideaCompass = extractSection(markdown, "Idea Compass"); var compassValues = extractBullets(ideaCompass); // TOP-CENTER: Book Title + Author ea.style.backgroundColor = lightHex(GRAY); ea.style.fontSize = 24; var bookLabel = bookTitle; if (bookAuthor) bookLabel += "\n" + bookAuthor; ea.addText(LEFT + 200, TOP - 110, bookLabel, { width: 600, textAlign: "center", box: "box", boxPadding: 14, boxStrokeColor: GRAY, }); // TOP-LEFT: Thesis highlight if (thesisSummary) { ea.style.backgroundColor = lightHex(GREEN); ea.style.fontSize = 13; ea.addText(LEFT - 280, TOP - 90, "THESIS\n" + thesisSummary, { width: 260, textAlign: "left", box: "box", boxPadding: 12, boxStrokeColor: GREEN, }); } // LEFT SIDE: Key Concepts (stacked) if (conceptHeadings.length > 0) { var conceptY = centerY + 10; for (var c = 0; c < conceptHeadings.length && c < 6; c++) { ea.style.backgroundColor = lightHex(BLUE); ea.style.fontSize = 12; ea.addText(LEFT - 260, conceptY, conceptHeadings[c], { width: 240, textAlign: "center", box: "box", boxPadding: 8, boxStrokeColor: BLUE, }); conceptY += 55; } } // RIGHT SIDE: Reflections ea.style.fontSize = 13; if (surprisedText) { ea.style.backgroundColor = lightHex(ORANGE); ea.addText(RIGHT + 20, centerY, "Surprised me:\n" + surprisedText, { width: 260, textAlign: "left", box: "box", boxPadding: 10, boxStrokeColor: ORANGE, }); } if (challengedText) { ea.style.backgroundColor = lightHex(ORANGE); ea.addText(RIGHT + 20, centerY + 130, "Challenged me:\n" + challengedText, { width: 260, textAlign: "left", box: "box", boxPadding: 10, boxStrokeColor: ORANGE, }); } // RIGHT SIDE LOWER: Empty takeaway zone ea.style.backgroundColor = lightHex(ORANGE); ea.addText(RIGHT + 20, centerY + 280, "My Takeaways\n\n\n", { width: 260, height: 120, textAlign: "left", textVerticalAlign: "top", box: "box", boxPadding: 10, boxStrokeColor: ORANGE, }); // BOTTOM-LEFT: Action Items (first 4) if (actions.length > 0) { ea.style.backgroundColor = lightHex(GREEN); ea.style.fontSize = 11; var actionText = actions.slice(0, 4).join("\n"); ea.addText(LEFT, BOTTOM + 20, "ACTION ITEMS\n" + actionText, { width: 400, textAlign: "left", box: "box", boxPadding: 10, boxStrokeColor: GREEN, }); } // BOTTOM-RIGHT: Idea Compass if (compassValues["Parents (origins)"] || compassValues["Friends (supporting ideas)"]) { ea.style.backgroundColor = lightHex(PURPLE); ea.style.fontSize = 10; var compassText = ""; if (compassValues["Parents (origins)"]) compassText += "Parents: " + compassValues["Parents (origins)"].substring(0, 100) + "\n"; if (compassValues["Friends (supporting ideas)"]) compassText += "Friends: " + compassValues["Friends (supporting ideas)"].substring(0, 100) + "\n"; if (compassValues["Children (applications)"]) compassText += "Children: " + compassValues["Children (applications)"].substring(0, 100); ea.addText(RIGHT - 420, BOTTOM + 20, compassText, { width: 420, textAlign: "left", box: "box", boxPadding: 10, boxStrokeColor: PURPLE, }); } // ============================================================ // GENERIC NOTE SCAFFOLDING // ============================================================ } else { var fileName = view.file.basename.replace(/ Visual$/, "").replace(/\.excalidraw$/, ""); // TOP-CENTER: File title ea.style.backgroundColor = lightHex(GRAY); ea.style.fontSize = 22; ea.addText(LEFT + 200, TOP - 80, fileName, { width: 600, textAlign: "center", box: "box", boxPadding: 14, boxStrokeColor: GRAY, }); // Distribute sections around the perimeter // Color cycle: green, blue, orange, purple, then repeat var colors = [GREEN, BLUE, ORANGE, PURPLE]; var sectionCount = h2s.length; if (sectionCount > 0) { // Split sections into left, right, and bottom groups var leftSections = []; var rightSections = []; var bottomSections = []; for (var s = 0; s < sectionCount; s++) { if (s < Math.ceil(sectionCount / 3)) { leftSections.push(h2s[s]); } else if (s < Math.ceil(2 * sectionCount / 3)) { rightSections.push(h2s[s]); } else { bottomSections.push(h2s[s]); } } // Left side var ly = centerY + 10; for (var l = 0; l < leftSections.length; l++) { var lColor = colors[l % colors.length]; ea.style.backgroundColor = lightHex(lColor); ea.style.fontSize = 12; var sectionPreview = firstLine(extractSection(markdown, leftSections[l].replace(/[.*+?^${}()|[\]\\]/g, "\\amp;"))); var label = leftSections[l]; if (sectionPreview) label += "\n" + sectionPreview; ea.addText(LEFT - 260, ly, label, { width: 240, textAlign: "left", box: "box", boxPadding: 10, boxStrokeColor: lColor, }); ly += 90; } // Right side var ry = centerY + 10; for (var r = 0; r < rightSections.length; r++) { var rColor = colors[(r + leftSections.length) % colors.length]; ea.style.backgroundColor = lightHex(rColor); ea.style.fontSize = 12; var rPreview = firstLine(extractSection(markdown, rightSections[r].replace(/[.*+?^${}()|[\]\\]/g, "\\amp;"))); var rLabel = rightSections[r]; if (rPreview) rLabel += "\n" + rPreview; ea.addText(RIGHT + 20, ry, rLabel, { width: 240, textAlign: "left", box: "box", boxPadding: 10, boxStrokeColor: rColor, }); ry += 90; } // Bottom var bx = LEFT; for (var b = 0; b < bottomSections.length; b++) { var bColor = colors[(b + leftSections.length + rightSections.length) % colors.length]; ea.style.backgroundColor = lightHex(bColor); ea.style.fontSize = 12; ea.addText(bx, BOTTOM + 20, bottomSections[b], { width: 260, textAlign: "center", box: "box", boxPadding: 10, boxStrokeColor: bColor, }); bx += 280; } } } await ea.addElementsToView(false, true, true); new Notice("Scaffolding added! (" + noteType + " mode)");