@weave-md/basic
Advanced tools
+21
| MIT License | ||
| Copyright (c) 2025 weavepage | ||
| 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. |
@@ -52,2 +52,3 @@ import { extractNodeLinks } from '@weave-md/validate'; | ||
| const inlineNodeIds = new Set(); | ||
| const stretchNodeIds = new Set(); | ||
| const footnoteNodeIds = new Set(); | ||
@@ -57,3 +58,3 @@ const findNodeRefs = (node) => { | ||
| const display = node.display || 'footnote'; | ||
| if (display === 'inline' || display === 'overlay' || display === 'footnote') { | ||
| if (display === 'inline' || display === 'overlay' || display === 'footnote' || display === 'stretch') { | ||
| referencedNodeIds.add(node.targetId); | ||
@@ -65,2 +66,4 @@ } | ||
| inlineNodeIds.add(node.targetId); | ||
| if (display === 'stretch') | ||
| stretchNodeIds.add(node.targetId); | ||
| if (display === 'footnote') | ||
@@ -137,2 +140,10 @@ footnoteNodeIds.add(node.targetId); | ||
| const mainNodeLinksHandler = (ref, text) => { | ||
| // Handle stretch display mode (Nutshell-style) | ||
| if (ref.display === 'stretch') { | ||
| const targetSection = sectionsMap[ref.id]; | ||
| if (targetSection) { | ||
| const displayText = text && text.trim() !== '' ? escapeHtml(text) : escapeHtml(ref.id); | ||
| return `<span class="weave-stretch-trigger" data-stretch-id="${escapeHtml(ref.id)}">${displayText}</span>`; | ||
| } | ||
| } | ||
| // Handle inline display mode | ||
@@ -185,2 +196,22 @@ if (ref.display === 'inline') { | ||
| }; | ||
| // nodeLinksHandler for stretch content (preserves stretch triggers, converts others to overlay) | ||
| const stretchNodeLinksHandler = (ref, text) => { | ||
| // Preserve stretch display mode for nested stretches | ||
| if (ref.display === 'stretch') { | ||
| const targetSection = sectionsMap[ref.id]; | ||
| if (targetSection) { | ||
| const displayText = text && text.trim() !== '' ? escapeHtml(text) : escapeHtml(ref.id); | ||
| return `<span class="weave-stretch-trigger" data-stretch-id="${escapeHtml(ref.id)}">${displayText}</span>`; | ||
| } | ||
| } | ||
| // Convert other display modes to overlay | ||
| const targetSection = sectionsMap[ref.id]; | ||
| if (!targetSection) { | ||
| return `<span class="weave-unsupported">${escapeHtml(text || ref.id)}</span>`; | ||
| } | ||
| if (!text || text.trim() === '') { | ||
| return `<span class="weave-overlay-anchor" data-display="overlay" data-node-id="${escapeHtml(ref.id)}" title="View ${escapeHtml(ref.id)}">${iconInformationCircle}</span>`; | ||
| } | ||
| return `<span class="weave-node-link" data-display="overlay" data-node-id="${escapeHtml(ref.id)}">${escapeHtml(text)}</span>`; | ||
| }; | ||
| // Render main sections first (to establish footnote order) | ||
@@ -211,3 +242,4 @@ const renderedSections = []; | ||
| } | ||
| // Build sectionsData for inline/overlay rendering | ||
| // Build sectionsData for inline/overlay/stretch rendering | ||
| // - Stretch content: preserves nested stretch triggers | ||
| // - Footnote/inline content: allows overlays only | ||
@@ -219,4 +251,10 @@ // - Overlay content: no nesting allowed | ||
| if (tree) { | ||
| // Use noNestingHandler for sections used as overlays | ||
| const handler = overlayNodeIds.has(section.id) ? noNestingHandler : nestedNodeLinksHandler; | ||
| // Choose handler based on how section is used | ||
| let handler = nestedNodeLinksHandler; | ||
| if (overlayNodeIds.has(section.id)) { | ||
| handler = noNestingHandler; | ||
| } | ||
| else if (stretchNodeIds.has(section.id)) { | ||
| handler = stretchNodeLinksHandler; | ||
| } | ||
| const sectionHtml = toHtml(tree, { | ||
@@ -223,0 +261,0 @@ renderMath: true, |
| /** | ||
| * Basic HTML/JS/CSS template for static export with footnote and overlay support | ||
| */ | ||
| export declare const HTML_TEMPLATE = "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>{{TITLE}}</title>\n <link rel=\"stylesheet\" href=\"https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.css\" integrity=\"sha384-n8MVd4RsNIU0tAv4ct0nTaAbDJwPJzDEaqSD1odI+WdtXRGWt2kTvGFasHpSy3SV\" crossorigin=\"anonymous\">\n <style>\n * {\n box-sizing: border-box;\n margin: 0;\n padding: 0;\n }\n\n body {\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;\n line-height: 1.6;\n color: #333;\n background: #fff;\n padding: 2rem 1rem;\n }\n\n .weave-document {\n max-width: 42rem;\n margin: 0 auto;\n position: relative;\n }\n\n .weave-section {\n margin-bottom: 3rem;\n }\n\n h1 {\n font-size: 2.25rem;\n font-weight: 700;\n }\n\n h2 {\n margin-top: 1.5rem;\n margin-bottom: 1rem;\n font-size: 1.75rem;\n font-weight: 600;\n }\n\n h3 {\n margin-top: 1.5rem;\n margin-bottom: 0.75rem;\n font-size: 1.25rem;\n font-weight: 600;\n }\n\n p {\n margin-bottom: 1rem;\n }\n\n /* Node links */\n .weave-node-link {\n color: #0066cc;\n text-decoration: none;\n border-bottom: 1px solid #0066cc;\n cursor: pointer;\n position: relative;\n }\n\n .weave-node-link:hover {\n background: #f0f7ff;\n }\n\n /* Overlay anchor icon (for empty text links) */\n .weave-icon {\n width: 1.2em;\n height: 1.2em;\n vertical-align: -0.2em;\n }\n\n .weave-overlay-anchor,\n .weave-inline-anchor {\n display: inline;\n color: #0066cc;\n cursor: pointer;\n }\n\n /* Plus/minus toggle for inline anchors */\n .weave-inline-anchor .weave-icon-minus {\n display: none;\n }\n\n .weave-inline-anchor.expanded .weave-icon-plus {\n display: none;\n }\n\n .weave-inline-anchor.expanded .weave-icon-minus {\n display: inline;\n }\n\n .weave-overlay-anchor:hover .weave-icon,\n .weave-inline-anchor:hover .weave-icon {\n fill: #f0f7ff;\n }\n\n /* Inline expandable content */\n .weave-inline-trigger {\n color: #0066cc;\n text-decoration: none;\n border-bottom: 1px solid #0066cc;\n cursor: pointer;\n }\n\n .weave-inline-trigger:hover {\n background: #f0f7ff;\n }\n\n .weave-inline-trigger.expanded {\n background: #e8f4ff;\n border-bottom: 2px solid #0066cc;\n }\n\n /* Inline substitution */\n .weave-sub {\n color: #0066cc;\n text-decoration: none;\n border-bottom: 1px solid #0066cc;\n cursor: pointer;\n }\n\n .weave-sub:hover {\n background: #f0f7ff;\n }\n\n .weave-sub.expanded {\n color: inherit;\n border-bottom: none;\n cursor: default;\n background: none;\n }\n\n /* Nested subs inside expanded subs should still be styled as links */\n .weave-sub.expanded .weave-sub:not(.expanded) {\n color: #0066cc;\n border-bottom: 1px solid #0066cc;\n cursor: pointer;\n }\n\n /* Redacted style - black blocks that are still clickable */\n .weave-sub-redacted:not(.expanded) {\n color: inherit;\n border-bottom: none;\n cursor: pointer;\n background: none;\n }\n\n .weave-sub-redacted:not(.expanded):hover {\n opacity: 0.7;\n }\n\n .weave-inline-content {\n display: block;\n margin: 1rem 0;\n padding: 1rem;\n background: #f8f9fa;\n border-left: 3px solid #0066cc;\n border-radius: 4px;\n }\n\n .weave-inline-content.hidden {\n display: none;\n }\n\n .weave-inline-content p:last-child {\n margin-bottom: 0;\n }\n\n /* Footnote references */\n .weave-footnote-ref {\n font-size: 0.75em;\n vertical-align: super;\n }\n\n .weave-footnote-ref a {\n color: #0066cc;\n text-decoration: none;\n }\n\n .weave-footnote-ref a:hover {\n background: #f0f7ff;\n }\n\n /* Text-linked footnote references */\n .weave-footnote-link {\n color: #0066cc;\n text-decoration: none;\n }\n\n .weave-footnote-link-text {\n border-bottom: 1px solid #0066cc;\n }\n\n .weave-footnote-link:hover {\n background: #f0f7ff;\n }\n\n .weave-footnote-link sup {\n font-size: 0.75em;\n margin-left: 0.1em;\n }\n\n /* Footnotes section */\n .weave-footnotes-separator {\n margin: 3rem 0 2rem;\n border: none;\n border-top: 1px solid #ddd;\n }\n\n .weave-footnotes {\n font-size: 0.9em;\n color: #666;\n }\n\n .weave-footnotes-list {\n list-style: none;\n padding: 0;\n }\n\n .weave-footnote {\n display: grid;\n grid-template-columns: 2.5em 1fr;\n margin-bottom: 1rem;\n }\n\n .weave-footnote-marker {\n text-align: left;\n }\n\n .weave-footnote-backref {\n text-decoration: none;\n color: #0066cc;\n }\n\n .weave-footnote-backref:hover {\n background: #f0f7ff;\n }\n\n .weave-footnote-content {\n min-width: 0;\n }\n\n .weave-footnote-content p:first-child {\n display: inline;\n }\n\n .weave-footnote-content p + p {\n margin-top: 0.5rem;\n }\n\n /* Overlay - bigfoot-style tooltip */\n .weave-overlay {\n position: fixed;\n z-index: 10000;\n box-sizing: border-box;\n max-width: min(22rem, calc(100vw - 20px));\n display: inline-block;\n background: #fafafa;\n border-radius: 0.5em;\n border: 1px solid #c3c3c3;\n box-shadow: 0px 0px 8px rgba(0, 0, 0, 0.3);\n opacity: 0;\n transform: scale(0.1) translateZ(0);\n transform-origin: 50% 0;\n transition: opacity 0.25s ease, transform 0.25s ease;\n pointer-events: none;\n }\n\n .weave-overlay.active {\n opacity: 0.97;\n transform: scale(1) translateZ(0);\n pointer-events: auto;\n }\n\n .weave-overlay.above {\n transform-origin: 50% 100%;\n }\n\n /* Tooltip arrow */\n .weave-overlay-tooltip {\n position: absolute;\n width: 0;\n height: 0;\n border-left: 10px solid transparent;\n border-right: 10px solid transparent;\n }\n\n .weave-overlay.below .weave-overlay-tooltip {\n top: -10px;\n border-bottom: 10px solid #c3c3c3;\n }\n\n .weave-overlay.below .weave-overlay-tooltip::after {\n content: '';\n position: absolute;\n top: 2px;\n left: -9px;\n border-left: 9px solid transparent;\n border-right: 9px solid transparent;\n border-bottom: 9px solid #fafafa;\n }\n\n .weave-overlay.above .weave-overlay-tooltip {\n bottom: -10px;\n border-top: 10px solid #c3c3c3;\n }\n\n .weave-overlay.above .weave-overlay-tooltip::after {\n content: '';\n position: absolute;\n bottom: 2px;\n left: -9px;\n border-left: 9px solid transparent;\n border-right: 9px solid transparent;\n border-top: 9px solid #fafafa;\n }\n\n .weave-overlay-content {\n position: relative;\n }\n\n .weave-overlay-main-wrapper {\n max-height: 15em;\n overflow: auto;\n }\n\n .weave-overlay-body {\n padding: 0.6em 0.8em;\n line-height: 1.5;\n font-size: 0.95em;\n color: #333;\n }\n\n .weave-overlay-body p {\n margin: 0;\n }\n\n .weave-overlay-body p + p {\n margin-top: 0.5em;\n }\n\n /* Math blocks */\n .weave-math-block {\n margin: 1.5rem 0;\n overflow-x: auto;\n }\n\n /* Media */\n .weave-media {\n margin: 1.5rem auto;\n text-align: center;\n }\n\n .weave-media img,\n .weave-media video,\n .weave-media iframe {\n max-width: 100%;\n width: 100%;\n height: auto;\n display: block;\n margin: 0 auto;\n }\n\n .weave-media video {\n background: #000;\n }\n\n .weave-media iframe {\n background: #000;\n }\n\n /* Fallback aspect ratio for embeds without explicit width/height */\n .weave-media iframe:not([width]):not([height]) {\n aspect-ratio: 16 / 9;\n }\n\n .weave-media figcaption {\n margin-top: 0.5rem;\n font-size: 0.9em;\n color: #666;\n font-style: italic;\n }\n\n /* Gallery Carousel */\n .weave-gallery {\n position: relative;\n overflow: hidden;\n }\n\n .weave-gallery figure {\n display: none;\n margin: 0;\n }\n\n .weave-gallery figure.active {\n display: block;\n }\n\n .weave-gallery img {\n border-radius: 4px;\n }\n\n .weave-gallery-nav {\n position: absolute;\n top: 50%;\n transform: translateY(-50%);\n background: rgba(0,0,0,0.5);\n color: white;\n border: none;\n padding: 0.75rem;\n cursor: pointer;\n font-size: 1.25rem;\n border-radius: 4px;\n z-index: 10;\n }\n\n .weave-gallery-nav:hover {\n background: rgba(0,0,0,0.7);\n }\n\n .weave-gallery-prev { left: 0.5rem; }\n .weave-gallery-next { right: 0.5rem; }\n\n .weave-gallery-dots {\n display: flex;\n justify-content: center;\n gap: 0.5rem;\n margin-top: 0.75rem;\n }\n\n .weave-gallery-dot {\n width: 8px;\n height: 8px;\n border-radius: 50%;\n background: #ccc;\n border: none;\n cursor: pointer;\n padding: 0;\n }\n\n .weave-gallery-dot.active {\n background: #0066cc;\n }\n\n /* Tables */\n table {\n border-collapse: collapse;\n width: 100%;\n margin: 1rem 0;\n }\n\n th, td {\n border: 1px solid #ddd;\n padding: 0.5rem 0.75rem;\n text-align: left;\n }\n\n th {\n background: #f5f5f5;\n font-weight: 600;\n }\n\n tr:nth-child(even) {\n background: #fafafa;\n }\n\n /* Code blocks */\n pre {\n background: #f5f5f5;\n padding: 1rem;\n border-radius: 4px;\n overflow-x: auto;\n margin: 1rem 0;\n }\n\n code {\n font-family: 'Monaco', 'Menlo', 'Courier New', monospace;\n font-size: 0.9em;\n }\n\n /* Preformatted - preserves spacing, matches paragraph spacing */\n .weave-preformatted {\n white-space: pre-wrap;\n margin-bottom: 1rem;\n }\n\n /* Mobile */\n @media (max-width: 640px) {\n body {\n padding: 1rem 0.75rem;\n }\n }\n </style>\n</head>\n<body>\n <main class=\"weave-document\">\n {{CONTENT}}\n </main>\n\n <!-- Overlay container -->\n <div class=\"weave-overlay\" id=\"weave-overlay\">\n <div class=\"weave-overlay-main-wrapper\">\n <div class=\"weave-overlay-body\" id=\"weave-overlay-body\"></div>\n </div>\n <div class=\"weave-overlay-tooltip\" id=\"weave-overlay-tooltip\"></div>\n </div>\n\n <script>\n // Section content lookup\n const sections = {{SECTIONS_DATA}};\n\n // Overlay handling\n const overlay = document.getElementById('weave-overlay');\n const overlayBody = document.getElementById('weave-overlay-body');\n\n let currentTrigger = null;\n\n const overlayTooltip = document.getElementById('weave-overlay-tooltip');\n\n function positionOverlay() {\n if (!currentTrigger) return;\n \n // Use getClientRects() to handle wrapped inline elements\n // Pick the last rect (end of link) for better UX\n const rects = currentTrigger.getClientRects();\n const rect = rects.length > 0 ? rects[rects.length - 1] : currentTrigger.getBoundingClientRect();\n const viewportHeight = window.innerHeight;\n \n // Use content container bounds instead of viewport for horizontal positioning\n // This keeps overlay within content area, leaving margins free for side notes\n const container = document.querySelector('.weave-document');\n const containerRect = container.getBoundingClientRect();\n \n const overlayHeight = overlay.offsetHeight;\n const overlayWidth = overlay.offsetWidth;\n \n // Trigger center is the anchor - arrow MUST point here\n const triggerCenterX = rect.left + (rect.width / 2);\n \n // Check space above and below\n const spaceBelow = viewportHeight - rect.bottom;\n const spaceAbove = rect.top;\n const showBelow = spaceBelow >= overlayHeight + 15 || spaceBelow > spaceAbove;\n \n const arrowMinEdge = 15; // min distance from arrow center to overlay edge\n const screenEdgePadding = 8; // minimum distance from screen edge for shadow visibility\n \n // Bounds: prefer container, but always keep minimum distance from screen edges\n const boundsLeft = Math.max(screenEdgePadding, containerRect.left);\n const boundsRight = Math.min(window.innerWidth - screenEdgePadding, containerRect.right);\n \n // Position overlay so arrow can reach the trigger\n // Arrow must be at triggerCenterX, and arrow must be within [arrowMinEdge, overlayWidth - arrowMinEdge]\n let leftMin = triggerCenterX - (overlayWidth - arrowMinEdge);\n let leftMax = triggerCenterX - arrowMinEdge;\n \n // Start centered on trigger\n let left = triggerCenterX - (overlayWidth / 2);\n \n // Ensure arrow can reach trigger (clamp to valid range)\n left = Math.max(leftMin, Math.min(leftMax, left));\n \n // Now clamp to container bounds\n // Clamp right first, then left (left takes priority so shadow is visible)\n if (left + overlayWidth > boundsRight) {\n left = boundsRight - overlayWidth;\n }\n if (left < boundsLeft) {\n left = boundsLeft;\n }\n \n overlay.style.left = left + 'px';\n \n // Arrow position = trigger center relative to overlay left edge\n const arrowLeftPx = triggerCenterX - left;\n overlayTooltip.style.left = arrowLeftPx + 'px';\n overlayTooltip.style.transform = 'translateX(-50%)';\n \n // Position vertically\n overlay.classList.remove('above', 'below');\n if (showBelow) {\n overlay.style.top = (rect.bottom + 10) + 'px';\n overlay.classList.add('below');\n } else {\n overlay.style.top = (rect.top - overlayHeight - 10) + 'px';\n overlay.classList.add('above');\n }\n }\n\n function openOverlay(sectionId, triggerElement) {\n const section = sections[sectionId];\n if (!section) return;\n\n overlayBody.innerHTML = section.html;\n currentTrigger = triggerElement;\n \n overlay.classList.add('active');\n positionOverlay();\n }\n\n // Reposition on scroll/resize to keep arrow attached\n window.addEventListener('scroll', () => {\n if (overlay.classList.contains('active')) {\n positionOverlay();\n }\n }, true);\n \n window.addEventListener('resize', () => {\n if (overlay.classList.contains('active')) {\n positionOverlay();\n }\n });\n\n function closeOverlay() {\n overlay.classList.remove('active');\n currentTrigger = null;\n }\n\n // Click handlers\n overlay.addEventListener('click', (e) => {\n if (e.target === overlay) closeOverlay();\n });\n\n // Escape key\n document.addEventListener('keydown', (e) => {\n if (e.key === 'Escape' && overlay.classList.contains('active')) {\n closeOverlay();\n }\n });\n\n // Click to open overlay\n document.addEventListener('click', (e) => {\n const link = e.target.closest('.weave-node-link, .weave-overlay-anchor');\n \n // Close overlay if clicking outside\n if (!link && !e.target.closest('.weave-overlay')) {\n if (overlay.classList.contains('active')) {\n closeOverlay();\n }\n return;\n }\n \n if (!link) return;\n\n const display = link.dataset.display;\n const nodeId = link.dataset.nodeId;\n\n if (display === 'overlay' && nodeId) {\n e.preventDefault();\n \n // Toggle overlay if clicking same trigger\n if (overlay.classList.contains('active') && currentTrigger === link) {\n closeOverlay();\n } else {\n openOverlay(nodeId, link);\n }\n }\n });\n\n // Inline expand/collapse handling\n document.addEventListener('click', (e) => {\n const trigger = e.target.closest('.weave-inline-trigger, .weave-inline-anchor');\n if (!trigger) return;\n\n e.preventDefault();\n \n const inlineId = trigger.dataset.inlineId;\n let content = document.getElementById('weave-inline-' + inlineId);\n \n // If content doesn't exist, create it\n if (!content) {\n const section = sections[inlineId];\n if (!section) return;\n \n content = document.createElement('div');\n content.id = 'weave-inline-' + inlineId;\n content.className = 'weave-inline-content';\n content.innerHTML = section.html;\n content.style.display = 'none';\n \n // Insert after the parent paragraph\n const paragraph = trigger.closest('p');\n if (paragraph && paragraph.parentNode) {\n paragraph.parentNode.insertBefore(content, paragraph.nextSibling);\n }\n }\n \n // Toggle visibility\n const isHidden = content.style.display === 'none';\n content.style.display = isHidden ? 'block' : 'none';\n trigger.classList.toggle('expanded', isHidden);\n });\n\n // Inline substitution click handling\n document.addEventListener('click', (e) => {\n const sub = e.target.closest('.weave-sub');\n if (!sub || sub.classList.contains('expanded')) return;\n\n e.preventDefault();\n const encodedReplacement = sub.dataset.replacementB64;\n if (encodedReplacement) {\n // Decode base64 with proper UTF-8 handling\n const binary = atob(encodedReplacement);\n const bytes = new Uint8Array(binary.length);\n for (let i = 0; i < binary.length; i++) {\n bytes[i] = binary.charCodeAt(i);\n }\n const replacement = new TextDecoder().decode(bytes);\n sub.innerHTML = replacement;\n sub.classList.add('expanded');\n }\n });\n\n // Footnote backlink tracking - remember which reference was clicked\n document.addEventListener('click', (e) => {\n const link = e.target.closest('.weave-footnote-ref a, a.weave-footnote-link');\n if (!link) return;\n \n const refId = link.id;\n if (!refId) return;\n \n // Extract footnote number from the href (e.g., #fn-3 -> 3)\n const href = link.getAttribute('href');\n if (!href || !href.startsWith('#fn-')) return;\n const fnNum = href.replace('#fn-', '');\n \n // Find the backref link in the footnote and update it\n const backref = document.querySelector('#fn-' + fnNum + ' .weave-footnote-backref');\n if (backref) {\n backref.setAttribute('href', '#' + refId);\n }\n \n // Navigate to the footnote\n e.preventDefault();\n const footnoteId = 'fn-' + fnNum;\n const footnote = document.getElementById(footnoteId);\n if (footnote) {\n footnote.scrollIntoView({ behavior: 'smooth' });\n // Update URL fragment without triggering navigation\n history.pushState(null, '', '#' + footnoteId);\n }\n });\n\n // Backref click - scroll and clear hash\n document.addEventListener('click', (e) => {\n const backref = e.target.closest('.weave-footnote-backref');\n if (!backref) return;\n \n e.preventDefault();\n const href = backref.getAttribute('href');\n if (!href) return;\n \n const target = document.getElementById(href.replace('#', ''));\n if (target) {\n target.scrollIntoView({ behavior: 'smooth' });\n history.pushState(null, '', window.location.pathname + window.location.search);\n }\n });\n\n // Video start time handling\n document.querySelectorAll('video[data-start]').forEach(video => {\n const startTime = parseFloat(video.dataset.start);\n if (!isNaN(startTime)) {\n video.currentTime = startTime;\n video.addEventListener('loadedmetadata', () => {\n video.currentTime = startTime;\n }, { once: true });\n }\n });\n\n // Gallery carousel initialization\n document.querySelectorAll('.weave-gallery').forEach(gallery => {\n const figures = gallery.querySelectorAll('figure');\n if (figures.length <= 1) return;\n \n // Set first figure as active\n figures[0].classList.add('active');\n \n // Create navigation buttons\n const prevBtn = document.createElement('button');\n prevBtn.className = 'weave-gallery-nav weave-gallery-prev';\n prevBtn.innerHTML = '❮';\n prevBtn.setAttribute('aria-label', 'Previous');\n \n const nextBtn = document.createElement('button');\n nextBtn.className = 'weave-gallery-nav weave-gallery-next';\n nextBtn.innerHTML = '❯';\n nextBtn.setAttribute('aria-label', 'Next');\n \n gallery.insertBefore(prevBtn, gallery.firstChild);\n gallery.insertBefore(nextBtn, gallery.querySelector('figcaption') || null);\n \n // Create dots\n const dotsContainer = document.createElement('div');\n dotsContainer.className = 'weave-gallery-dots';\n figures.forEach((_, i) => {\n const dot = document.createElement('button');\n dot.className = 'weave-gallery-dot' + (i === 0 ? ' active' : '');\n dot.setAttribute('aria-label', 'Go to slide ' + (i + 1));\n dot.dataset.index = i;\n dotsContainer.appendChild(dot);\n });\n const caption = gallery.querySelector('figcaption');\n if (caption) {\n gallery.insertBefore(dotsContainer, caption);\n } else {\n gallery.appendChild(dotsContainer);\n }\n \n let current = 0;\n \n const showSlide = (index) => {\n figures.forEach((f, i) => f.classList.toggle('active', i === index));\n dotsContainer.querySelectorAll('.weave-gallery-dot').forEach((d, i) => \n d.classList.toggle('active', i === index)\n );\n current = index;\n };\n \n prevBtn.addEventListener('click', () => {\n showSlide((current - 1 + figures.length) % figures.length);\n });\n \n nextBtn.addEventListener('click', () => {\n showSlide((current + 1) % figures.length);\n });\n \n dotsContainer.addEventListener('click', (e) => {\n const dot = e.target.closest('.weave-gallery-dot');\n if (dot) showSlide(parseInt(dot.dataset.index));\n });\n });\n </script>\n</body>\n</html>\n"; | ||
| export declare const HTML_TEMPLATE = "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>{{TITLE}}</title>\n <link rel=\"stylesheet\" href=\"https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.css\" integrity=\"sha384-n8MVd4RsNIU0tAv4ct0nTaAbDJwPJzDEaqSD1odI+WdtXRGWt2kTvGFasHpSy3SV\" crossorigin=\"anonymous\">\n <style>\n * {\n box-sizing: border-box;\n margin: 0;\n padding: 0;\n }\n\n body {\n font-family: 'Iowan Old Style', 'Cambria', 'Palatino Linotype', Palatino, Georgia, serif;\n font-size: 20px;\n font-weight: 300;\n line-height: 1.6;\n letter-spacing: 0.01em;\n color: #363737;\n background: #fff;\n padding: 2rem 1rem;\n }\n\n .weave-document {\n max-width: 42rem;\n margin: 0 auto;\n position: relative;\n }\n\n .weave-section {\n margin-bottom: 3rem;\n }\n\n h1, h2, h3, h4, h5, h6 {\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Helvetica Neue', Helvetica, Arial, 'Liberation Sans', sans-serif;\n letter-spacing: -0.02em;\n }\n\n /* Reduce top margin when header follows another header */\n h1 + h2, h2 + h3, h3 + h4, h4 + h5, h5 + h6 {\n margin-top: 4px;\n }\n\n h1 {\n font-size: 32px;\n font-weight: 700;\n margin-bottom: 20px;\n }\n\n h2 {\n margin-top: 32px;\n margin-bottom: 20px;\n font-size: 28px;\n font-weight: 600;\n }\n\n h3 {\n margin-top: 26px;\n margin-bottom: 16px;\n font-size: 26px;\n font-weight: 600;\n }\n \n h4 {\n margin-top: 22px;\n margin-bottom: 14px;\n font-size: 24px;\n font-weight: 600;\n }\n \n h5 {\n margin-top: 18px;\n margin-bottom: 12px;\n font-size: 22px;\n font-weight: 500;\n }\n \n h6 {\n margin-top: 16px;\n margin-bottom: 10px;\n font-size: 20px;\n font-weight: 500;\n }\n\n p {\n margin-bottom: 16px;\n }\n\n blockquote {\n padding-left: 20px;\n border-left: 3px solid #0066cc;\n margin-top: 20px;\n }\n\n /* Node links */\n .weave-node-link {\n color: #0066cc;\n text-decoration: none;\n border-bottom: 1px solid #0066cc;\n cursor: pointer;\n position: relative;\n }\n\n .weave-node-link:hover {\n background: #f0f7ff;\n }\n\n /* Overlay anchor icon (for empty text links) */\n .weave-icon {\n width: 1.2em;\n height: 1.2em;\n vertical-align: -0.2em;\n }\n\n .weave-overlay-anchor,\n .weave-inline-anchor {\n display: inline;\n color: #0066cc;\n cursor: pointer;\n }\n\n /* Plus/minus toggle for inline anchors */\n .weave-inline-anchor .weave-icon-minus {\n display: none;\n }\n\n .weave-inline-anchor.expanded .weave-icon-plus {\n display: none;\n }\n\n .weave-inline-anchor.expanded .weave-icon-minus {\n display: inline;\n }\n\n .weave-overlay-anchor:hover .weave-icon,\n .weave-inline-anchor:hover .weave-icon {\n fill: #f0f7ff;\n }\n\n /* Inline expandable content */\n .weave-inline-trigger {\n color: #0066cc;\n text-decoration: none;\n border-bottom: 1px solid #0066cc;\n cursor: pointer;\n }\n\n .weave-inline-trigger:hover {\n background: #f0f7ff;\n }\n\n .weave-inline-trigger.expanded {\n background: #e8f4ff;\n border-bottom: 2px solid #0066cc;\n }\n\n /* Inline substitution */\n .weave-sub {\n color: #0066cc;\n text-decoration: none;\n border-bottom: 1px solid #0066cc;\n cursor: pointer;\n }\n\n .weave-sub:hover {\n background: #f0f7ff;\n }\n\n .weave-sub.expanded {\n color: inherit;\n border-bottom: none;\n cursor: default;\n background: none;\n }\n\n /* Nested subs inside expanded subs should still be styled as links */\n .weave-sub.expanded .weave-sub:not(.expanded) {\n color: #0066cc;\n border-bottom: 1px solid #0066cc;\n cursor: pointer;\n }\n\n /* Redacted style - black blocks that are still clickable */\n .weave-sub-redacted:not(.expanded) {\n color: inherit;\n border-bottom: none;\n cursor: pointer;\n background: none;\n }\n\n .weave-sub-redacted:not(.expanded):hover {\n opacity: 0.7;\n }\n\n .weave-inline-content {\n display: block;\n margin: 1rem 0;\n padding: 1rem;\n background: #f8f9fa;\n border-left: 3px solid #0066cc;\n border-radius: 4px;\n }\n\n .weave-inline-content.hidden {\n display: none;\n }\n\n .weave-inline-content p:last-child {\n margin-bottom: 0;\n }\n\n /* Stretch/Nutshell-style expandable content */\n .weave-stretch-trigger {\n color: #2b67ad;\n text-decoration: none;\n border-bottom: 2px dotted #2b67ad;\n cursor: pointer;\n position: relative;\n }\n\n .weave-stretch-trigger:hover {\n background: rgba(43, 103, 173, 0.1);\n }\n\n .weave-stretch-trigger.expanded {\n border-bottom-style: solid;\n }\n\n .weave-stretch-bubble {\n display: block;\n position: relative;\n margin-top: 20px;\n transition: opacity 0.3s ease-out, margin 0.3s ease-out;\n opacity: 0;\n margin-bottom: 0;\n max-height: 0;\n overflow: hidden;\n }\n\n .weave-stretch-bubble.open {\n max-height: none;\n overflow: visible;\n opacity: 1;\n margin-bottom: 0.75rem;\n }\n\n .weave-stretch-content {\n background: #fff;\n border: 2px solid #ccc;\n border-radius: 1rem;\n padding: 1rem 0.8rem;\n position: relative;\n box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);\n }\n\n /* Arrow pointing up to trigger (Nutshell-style) */\n .weave-stretch-arrow {\n width: 0;\n height: 0;\n border-left: 20px solid transparent;\n border-right: 20px solid transparent;\n border-bottom: 20px solid #ccc;\n position: absolute;\n top: -20px;\n pointer-events: none;\n }\n\n .weave-stretch-arrow::after {\n content: '';\n width: 0;\n height: 0;\n border-left: 20px solid transparent;\n border-right: 20px solid transparent;\n border-bottom: 20px solid #fff;\n position: absolute;\n top: 2px;\n left: -20px;\n pointer-events: none;\n }\n\n .weave-stretch-content p:first-child {\n margin-top: 0;\n }\n\n .weave-stretch-content p:last-child {\n margin-bottom: 0;\n }\n\n .weave-stretch-content ul,\n .weave-stretch-content ol {\n padding-left: 1.5rem;\n margin: 0.5rem 0;\n }\n\n /* Nested stretch bubbles - slightly different style */\n .weave-stretch-content .weave-stretch-content {\n background: #fafafa;\n border-color: #ddd;\n }\n\n .weave-stretch-content .weave-stretch-arrow {\n border-bottom-color: #ddd;\n }\n\n .weave-stretch-content .weave-stretch-arrow::after {\n border-bottom-color: #fafafa;\n }\n\n /* Level 3+ nesting */\n .weave-stretch-content .weave-stretch-content .weave-stretch-content {\n background: #f5f5f5;\n border-color: #e0e0e0;\n }\n\n .weave-stretch-content .weave-stretch-content .weave-stretch-arrow {\n border-bottom-color: #e0e0e0;\n }\n\n .weave-stretch-content .weave-stretch-content .weave-stretch-arrow::after {\n border-bottom-color: #f5f5f5;\n }\n\n /* Footnote references */\n .weave-footnote-ref {\n font-size: 0.75em;\n vertical-align: super;\n }\n\n .weave-footnote-ref a {\n color: #0066cc;\n text-decoration: none;\n }\n\n .weave-footnote-ref a:hover {\n background: #f0f7ff;\n }\n\n /* Text-linked footnote references */\n .weave-footnote-link {\n color: #0066cc;\n text-decoration: none;\n }\n\n .weave-footnote-link-text {\n border-bottom: 1px solid #0066cc;\n }\n\n .weave-footnote-link:hover {\n background: #f0f7ff;\n }\n\n .weave-footnote-link sup {\n font-size: 0.75em;\n margin-left: 0.1em;\n }\n\n /* Footnotes section */\n .weave-footnotes-separator {\n margin: 3rem 0 2rem;\n border: none;\n border-top: 1px solid #ddd;\n }\n\n .weave-footnotes {\n font-size: 0.9em;\n color: #666;\n }\n\n .weave-footnotes-list {\n list-style: none;\n padding: 0;\n }\n\n .weave-footnote {\n display: grid;\n grid-template-columns: 2.5em 1fr;\n margin-bottom: 1rem;\n }\n\n .weave-footnote-marker {\n text-align: left;\n }\n\n .weave-footnote-backref {\n text-decoration: none;\n color: #0066cc;\n }\n\n .weave-footnote-backref:hover {\n background: #f0f7ff;\n }\n\n .weave-footnote-content {\n min-width: 0;\n }\n\n .weave-footnote-content p:first-child {\n display: inline;\n }\n\n .weave-footnote-content p + p {\n margin-top: 0.5rem;\n }\n\n /* Overlay - bigfoot-style tooltip */\n .weave-overlay {\n position: fixed;\n z-index: 10000;\n box-sizing: border-box;\n max-width: min(22rem, calc(100vw - 20px));\n display: inline-block;\n background: #fafafa;\n border-radius: 0.5em;\n border: 1px solid #c3c3c3;\n box-shadow: 0px 0px 8px rgba(0, 0, 0, 0.3);\n opacity: 0;\n transform: scale(0.1) translateZ(0);\n transform-origin: 50% 0;\n transition: opacity 0.25s ease, transform 0.25s ease;\n pointer-events: none;\n }\n\n .weave-overlay.active {\n opacity: 0.97;\n transform: scale(1) translateZ(0);\n pointer-events: auto;\n }\n\n .weave-overlay.above {\n transform-origin: 50% 100%;\n }\n\n /* Tooltip arrow */\n .weave-overlay-tooltip {\n position: absolute;\n width: 0;\n height: 0;\n border-left: 10px solid transparent;\n border-right: 10px solid transparent;\n }\n\n .weave-overlay.below .weave-overlay-tooltip {\n top: -10px;\n border-bottom: 10px solid #c3c3c3;\n }\n\n .weave-overlay.below .weave-overlay-tooltip::after {\n content: '';\n position: absolute;\n top: 2px;\n left: -9px;\n border-left: 9px solid transparent;\n border-right: 9px solid transparent;\n border-bottom: 9px solid #fafafa;\n }\n\n .weave-overlay.above .weave-overlay-tooltip {\n bottom: -10px;\n border-top: 10px solid #c3c3c3;\n }\n\n .weave-overlay.above .weave-overlay-tooltip::after {\n content: '';\n position: absolute;\n bottom: 2px;\n left: -9px;\n border-left: 9px solid transparent;\n border-right: 9px solid transparent;\n border-top: 9px solid #fafafa;\n }\n\n .weave-overlay-content {\n position: relative;\n }\n\n .weave-overlay-main-wrapper {\n max-height: 15em;\n overflow: auto;\n }\n\n .weave-overlay-body {\n padding: 0.6em 0.8em;\n line-height: 1.5;\n font-size: 0.95em;\n color: #333;\n }\n\n .weave-overlay-body p {\n margin: 0;\n }\n\n .weave-overlay-body p + p {\n margin-top: 0.5em;\n }\n\n /* Math blocks */\n .weave-math-block {\n margin: 1.5rem 0;\n overflow-x: auto;\n }\n\n /* Media */\n .weave-media {\n margin: 1.5rem auto;\n text-align: center;\n }\n\n .weave-media img,\n .weave-media video,\n .weave-media iframe {\n max-width: 100%;\n width: 100%;\n height: auto;\n display: block;\n margin: 0 auto;\n }\n\n .weave-media video {\n background: #000;\n }\n\n .weave-media iframe {\n background: #000;\n }\n\n /* Fallback aspect ratio for embeds without explicit width/height */\n .weave-media iframe:not([width]):not([height]) {\n aspect-ratio: 16 / 9;\n }\n\n .weave-media figcaption {\n margin-top: 0.5rem;\n font-size: 0.9em;\n color: #666;\n font-style: italic;\n }\n\n /* Gallery Carousel */\n .weave-gallery {\n position: relative;\n overflow: hidden;\n }\n\n .weave-gallery figure {\n display: none;\n margin: 0;\n }\n\n .weave-gallery figure.active {\n display: block;\n }\n\n .weave-gallery img {\n border-radius: 4px;\n }\n\n .weave-gallery-nav {\n position: absolute;\n top: 50%;\n transform: translateY(-50%);\n background: rgba(0,0,0,0.5);\n color: white;\n border: none;\n padding: 0.75rem;\n cursor: pointer;\n font-size: 1.25rem;\n border-radius: 4px;\n z-index: 10;\n }\n\n .weave-gallery-nav:hover {\n background: rgba(0,0,0,0.7);\n }\n\n .weave-gallery-prev { left: 0.5rem; }\n .weave-gallery-next { right: 0.5rem; }\n\n .weave-gallery-dots {\n display: flex;\n justify-content: center;\n gap: 0.5rem;\n margin-top: 0.75rem;\n }\n\n .weave-gallery-dot {\n width: 8px;\n height: 8px;\n border-radius: 50%;\n background: #ccc;\n border: none;\n cursor: pointer;\n padding: 0;\n }\n\n .weave-gallery-dot.active {\n background: #0066cc;\n }\n\n /* Tables */\n table {\n border-collapse: collapse;\n width: 100%;\n margin: 1rem 0;\n }\n\n th, td {\n border: 1px solid #ddd;\n padding: 0.5rem 0.75rem;\n text-align: left;\n }\n\n th {\n background: #f5f5f5;\n font-weight: 600;\n }\n\n tr:nth-child(even) {\n background: #fafafa;\n }\n\n /* Task lists */\n ul:has(input[type=\"checkbox\"]) {\n list-style: none;\n padding-left: 0;\n }\n\n ul:has(input[type=\"checkbox\"]) ul {\n list-style: none;\n padding-left: 1.5em;\n }\n\n li:has(> input[type=\"checkbox\"]) {\n margin-bottom: 0.25em;\n }\n\n input[type=\"checkbox\"] {\n margin-right: 0.5em;\n }\n\n /* Code blocks */\n pre {\n background: #f5f5f5;\n padding: 1rem;\n border-radius: 4px;\n overflow-x: auto;\n margin: 1rem 0;\n }\n\n code {\n font-family: 'Monaco', 'Menlo', 'Courier New', monospace;\n font-size: 0.9em;\n }\n\n /* Preformatted - preserves spacing, matches paragraph spacing */\n .weave-preformatted {\n white-space: pre-wrap;\n margin-bottom: 1rem;\n }\n\n /* Mobile */\n @media (max-width: 640px) {\n body {\n padding: 1rem 0.75rem;\n }\n }\n </style>\n</head>\n<body>\n <main class=\"weave-document\">\n {{CONTENT}}\n </main>\n\n <!-- Overlay container -->\n <div class=\"weave-overlay\" id=\"weave-overlay\">\n <div class=\"weave-overlay-main-wrapper\">\n <div class=\"weave-overlay-body\" id=\"weave-overlay-body\"></div>\n </div>\n <div class=\"weave-overlay-tooltip\" id=\"weave-overlay-tooltip\"></div>\n </div>\n\n <script>\n // Section content lookup\n const sections = {{SECTIONS_DATA}};\n\n // Overlay handling\n const overlay = document.getElementById('weave-overlay');\n const overlayBody = document.getElementById('weave-overlay-body');\n\n let currentTrigger = null;\n\n const overlayTooltip = document.getElementById('weave-overlay-tooltip');\n\n function positionOverlay() {\n if (!currentTrigger) return;\n \n // Use getClientRects() to handle wrapped inline elements\n // Pick the last rect (end of link) for better UX\n const rects = currentTrigger.getClientRects();\n const rect = rects.length > 0 ? rects[rects.length - 1] : currentTrigger.getBoundingClientRect();\n const viewportHeight = window.innerHeight;\n \n // Use content container bounds instead of viewport for horizontal positioning\n // This keeps overlay within content area, leaving margins free for side notes\n const container = document.querySelector('.weave-document');\n const containerRect = container.getBoundingClientRect();\n \n const overlayHeight = overlay.offsetHeight;\n const overlayWidth = overlay.offsetWidth;\n \n // Trigger center is the anchor - arrow MUST point here\n const triggerCenterX = rect.left + (rect.width / 2);\n \n // Check space above and below\n const spaceBelow = viewportHeight - rect.bottom;\n const spaceAbove = rect.top;\n const showBelow = spaceBelow >= overlayHeight + 15 || spaceBelow > spaceAbove;\n \n const arrowMinEdge = 15; // min distance from arrow center to overlay edge\n const screenEdgePadding = 8; // minimum distance from screen edge for shadow visibility\n \n // Bounds: prefer container, but always keep minimum distance from screen edges\n const boundsLeft = Math.max(screenEdgePadding, containerRect.left);\n const boundsRight = Math.min(window.innerWidth - screenEdgePadding, containerRect.right);\n \n // Position overlay so arrow can reach the trigger\n // Arrow must be at triggerCenterX, and arrow must be within [arrowMinEdge, overlayWidth - arrowMinEdge]\n let leftMin = triggerCenterX - (overlayWidth - arrowMinEdge);\n let leftMax = triggerCenterX - arrowMinEdge;\n \n // Start centered on trigger\n let left = triggerCenterX - (overlayWidth / 2);\n \n // Ensure arrow can reach trigger (clamp to valid range)\n left = Math.max(leftMin, Math.min(leftMax, left));\n \n // Now clamp to container bounds\n // Clamp right first, then left (left takes priority so shadow is visible)\n if (left + overlayWidth > boundsRight) {\n left = boundsRight - overlayWidth;\n }\n if (left < boundsLeft) {\n left = boundsLeft;\n }\n \n overlay.style.left = left + 'px';\n \n // Arrow position = trigger center relative to overlay left edge\n const arrowLeftPx = triggerCenterX - left;\n overlayTooltip.style.left = arrowLeftPx + 'px';\n overlayTooltip.style.transform = 'translateX(-50%)';\n \n // Position vertically\n overlay.classList.remove('above', 'below');\n if (showBelow) {\n overlay.style.top = (rect.bottom + 10) + 'px';\n overlay.classList.add('below');\n } else {\n overlay.style.top = (rect.top - overlayHeight - 10) + 'px';\n overlay.classList.add('above');\n }\n }\n\n function openOverlay(sectionId, triggerElement) {\n const section = sections[sectionId];\n if (!section) return;\n\n overlayBody.innerHTML = section.html;\n currentTrigger = triggerElement;\n \n overlay.classList.add('active');\n positionOverlay();\n }\n\n // Reposition on scroll/resize to keep arrow attached\n window.addEventListener('scroll', () => {\n if (overlay.classList.contains('active')) {\n positionOverlay();\n }\n }, true);\n \n window.addEventListener('resize', () => {\n if (overlay.classList.contains('active')) {\n positionOverlay();\n }\n });\n\n function closeOverlay() {\n overlay.classList.remove('active');\n currentTrigger = null;\n }\n\n // Click handlers\n overlay.addEventListener('click', (e) => {\n if (e.target === overlay) closeOverlay();\n });\n\n // Escape key\n document.addEventListener('keydown', (e) => {\n if (e.key === 'Escape' && overlay.classList.contains('active')) {\n closeOverlay();\n }\n });\n\n // Click to open overlay\n document.addEventListener('click', (e) => {\n const link = e.target.closest('.weave-node-link, .weave-overlay-anchor');\n \n // Close overlay if clicking outside\n if (!link && !e.target.closest('.weave-overlay')) {\n if (overlay.classList.contains('active')) {\n closeOverlay();\n }\n return;\n }\n \n if (!link) return;\n\n const display = link.dataset.display;\n const nodeId = link.dataset.nodeId;\n\n if (display === 'overlay' && nodeId) {\n e.preventDefault();\n \n // Toggle overlay if clicking same trigger\n if (overlay.classList.contains('active') && currentTrigger === link) {\n closeOverlay();\n } else {\n openOverlay(nodeId, link);\n }\n }\n });\n\n // Inline expand/collapse handling\n document.addEventListener('click', (e) => {\n const trigger = e.target.closest('.weave-inline-trigger, .weave-inline-anchor');\n if (!trigger) return;\n\n e.preventDefault();\n \n const inlineId = trigger.dataset.inlineId;\n let content = document.getElementById('weave-inline-' + inlineId);\n \n // If content doesn't exist, create it\n if (!content) {\n const section = sections[inlineId];\n if (!section) return;\n \n content = document.createElement('div');\n content.id = 'weave-inline-' + inlineId;\n content.className = 'weave-inline-content';\n content.innerHTML = section.html;\n content.style.display = 'none';\n \n // Insert after the parent paragraph\n const paragraph = trigger.closest('p');\n if (paragraph && paragraph.parentNode) {\n paragraph.parentNode.insertBefore(content, paragraph.nextSibling);\n }\n }\n \n // Toggle visibility\n const isHidden = content.style.display === 'none';\n content.style.display = isHidden ? 'block' : 'none';\n trigger.classList.toggle('expanded', isHidden);\n });\n\n // Inline substitution click handling\n document.addEventListener('click', (e) => {\n const sub = e.target.closest('.weave-sub');\n if (!sub || sub.classList.contains('expanded')) return;\n\n e.preventDefault();\n const encodedReplacement = sub.dataset.replacementB64;\n if (encodedReplacement) {\n // Decode base64 with proper UTF-8 handling\n const binary = atob(encodedReplacement);\n const bytes = new Uint8Array(binary.length);\n for (let i = 0; i < binary.length; i++) {\n bytes[i] = binary.charCodeAt(i);\n }\n const replacement = new TextDecoder().decode(bytes);\n sub.innerHTML = replacement;\n sub.classList.add('expanded');\n }\n });\n\n // Stretch/Nutshell-style expand/collapse handling\n document.addEventListener('click', (e) => {\n const trigger = e.target.closest('.weave-stretch-trigger');\n if (!trigger) return;\n\n e.preventDefault();\n \n const stretchId = trigger.dataset.stretchId;\n \n // Find or create bubble\n let bubble = trigger._bubble;\n \n if (!bubble) {\n const section = sections[stretchId];\n if (!section) return;\n \n bubble = document.createElement('div');\n bubble.className = 'weave-stretch-bubble';\n bubble.dataset.stretchId = stretchId;\n bubble.innerHTML = '<div class=\"weave-stretch-content\"><div class=\"weave-stretch-arrow\"></div>' + section.html + '</div>';\n \n // Insert bubble after the parent paragraph\n const paragraph = trigger.closest('p, li, td, div.weave-stretch-content');\n if (paragraph && paragraph.parentNode) {\n paragraph.parentNode.insertBefore(bubble, paragraph.nextSibling);\n } else {\n trigger.parentNode.insertBefore(bubble, trigger.nextSibling);\n }\n \n // Position arrow to point at trigger\n const arrow = bubble.querySelector('.weave-stretch-arrow');\n const triggerRect = trigger.getBoundingClientRect();\n const bubbleRect = bubble.getBoundingClientRect();\n const arrowLeft = triggerRect.left - bubbleRect.left + (triggerRect.width / 2) - 20; // -20 to center the arrow\n arrow.style.left = arrowLeft + 'px';\n \n trigger._bubble = bubble;\n \n // Force reflow for animation\n bubble.offsetHeight;\n }\n \n // Toggle open/close\n const isOpen = bubble.classList.contains('open');\n if (isOpen) {\n bubble.classList.remove('open');\n trigger.classList.remove('expanded');\n } else {\n bubble.classList.add('open');\n trigger.classList.add('expanded');\n }\n });\n\n // Footnote backlink tracking - remember which reference was clicked\n document.addEventListener('click', (e) => {\n const link = e.target.closest('.weave-footnote-ref a, a.weave-footnote-link');\n if (!link) return;\n \n const refId = link.id;\n if (!refId) return;\n \n // Extract footnote number from the href (e.g., #fn-3 -> 3)\n const href = link.getAttribute('href');\n if (!href || !href.startsWith('#fn-')) return;\n const fnNum = href.replace('#fn-', '');\n \n // Find the backref link in the footnote and update it\n const backref = document.querySelector('#fn-' + fnNum + ' .weave-footnote-backref');\n if (backref) {\n backref.setAttribute('href', '#' + refId);\n }\n \n // Navigate to the footnote\n e.preventDefault();\n const footnoteId = 'fn-' + fnNum;\n const footnote = document.getElementById(footnoteId);\n if (footnote) {\n footnote.scrollIntoView({ behavior: 'smooth' });\n // Update URL fragment without triggering navigation\n history.pushState(null, '', '#' + footnoteId);\n }\n });\n\n // Backref click - scroll and clear hash\n document.addEventListener('click', (e) => {\n const backref = e.target.closest('.weave-footnote-backref');\n if (!backref) return;\n \n e.preventDefault();\n const href = backref.getAttribute('href');\n if (!href) return;\n \n const target = document.getElementById(href.replace('#', ''));\n if (target) {\n target.scrollIntoView({ behavior: 'smooth' });\n history.pushState(null, '', window.location.pathname + window.location.search);\n }\n });\n\n // Video start time handling\n document.querySelectorAll('video[data-start]').forEach(video => {\n const startTime = parseFloat(video.dataset.start);\n if (!isNaN(startTime)) {\n video.currentTime = startTime;\n video.addEventListener('loadedmetadata', () => {\n video.currentTime = startTime;\n }, { once: true });\n }\n });\n\n // Gallery carousel initialization\n document.querySelectorAll('.weave-gallery').forEach(gallery => {\n const figures = gallery.querySelectorAll('figure');\n if (figures.length <= 1) return;\n \n // Set first figure as active\n figures[0].classList.add('active');\n \n // Create navigation buttons\n const prevBtn = document.createElement('button');\n prevBtn.className = 'weave-gallery-nav weave-gallery-prev';\n prevBtn.innerHTML = '❮';\n prevBtn.setAttribute('aria-label', 'Previous');\n \n const nextBtn = document.createElement('button');\n nextBtn.className = 'weave-gallery-nav weave-gallery-next';\n nextBtn.innerHTML = '❯';\n nextBtn.setAttribute('aria-label', 'Next');\n \n gallery.insertBefore(prevBtn, gallery.firstChild);\n gallery.insertBefore(nextBtn, gallery.querySelector('figcaption') || null);\n \n // Create dots\n const dotsContainer = document.createElement('div');\n dotsContainer.className = 'weave-gallery-dots';\n figures.forEach((_, i) => {\n const dot = document.createElement('button');\n dot.className = 'weave-gallery-dot' + (i === 0 ? ' active' : '');\n dot.setAttribute('aria-label', 'Go to slide ' + (i + 1));\n dot.dataset.index = i;\n dotsContainer.appendChild(dot);\n });\n const caption = gallery.querySelector('figcaption');\n if (caption) {\n gallery.insertBefore(dotsContainer, caption);\n } else {\n gallery.appendChild(dotsContainer);\n }\n \n let current = 0;\n \n const showSlide = (index) => {\n figures.forEach((f, i) => f.classList.toggle('active', i === index));\n dotsContainer.querySelectorAll('.weave-gallery-dot').forEach((d, i) => \n d.classList.toggle('active', i === index)\n );\n current = index;\n };\n \n prevBtn.addEventListener('click', () => {\n showSlide((current - 1 + figures.length) % figures.length);\n });\n \n nextBtn.addEventListener('click', () => {\n showSlide((current + 1) % figures.length);\n });\n \n dotsContainer.addEventListener('click', (e) => {\n const dot = e.target.closest('.weave-gallery-dot');\n if (dot) showSlide(parseInt(dot.dataset.index));\n });\n });\n </script>\n</body>\n</html>\n"; | ||
| export declare function renderTemplate(options: { | ||
@@ -6,0 +6,0 @@ title: string; |
+234
-10
@@ -19,5 +19,8 @@ /** | ||
| body { | ||
| font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; | ||
| font-family: 'Iowan Old Style', 'Cambria', 'Palatino Linotype', Palatino, Georgia, serif; | ||
| font-size: 20px; | ||
| font-weight: 300; | ||
| line-height: 1.6; | ||
| color: #333; | ||
| letter-spacing: 0.01em; | ||
| color: #363737; | ||
| background: #fff; | ||
@@ -37,11 +40,22 @@ padding: 2rem 1rem; | ||
| h1, h2, h3, h4, h5, h6 { | ||
| font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Helvetica Neue', Helvetica, Arial, 'Liberation Sans', sans-serif; | ||
| letter-spacing: -0.02em; | ||
| } | ||
| /* Reduce top margin when header follows another header */ | ||
| h1 + h2, h2 + h3, h3 + h4, h4 + h5, h5 + h6 { | ||
| margin-top: 4px; | ||
| } | ||
| h1 { | ||
| font-size: 2.25rem; | ||
| font-size: 32px; | ||
| font-weight: 700; | ||
| margin-bottom: 20px; | ||
| } | ||
| h2 { | ||
| margin-top: 1.5rem; | ||
| margin-bottom: 1rem; | ||
| font-size: 1.75rem; | ||
| margin-top: 32px; | ||
| margin-bottom: 20px; | ||
| font-size: 28px; | ||
| font-weight: 600; | ||
@@ -51,12 +65,39 @@ } | ||
| h3 { | ||
| margin-top: 1.5rem; | ||
| margin-bottom: 0.75rem; | ||
| font-size: 1.25rem; | ||
| margin-top: 26px; | ||
| margin-bottom: 16px; | ||
| font-size: 26px; | ||
| font-weight: 600; | ||
| } | ||
| h4 { | ||
| margin-top: 22px; | ||
| margin-bottom: 14px; | ||
| font-size: 24px; | ||
| font-weight: 600; | ||
| } | ||
| h5 { | ||
| margin-top: 18px; | ||
| margin-bottom: 12px; | ||
| font-size: 22px; | ||
| font-weight: 500; | ||
| } | ||
| h6 { | ||
| margin-top: 16px; | ||
| margin-bottom: 10px; | ||
| font-size: 20px; | ||
| font-weight: 500; | ||
| } | ||
| p { | ||
| margin-bottom: 1rem; | ||
| margin-bottom: 16px; | ||
| } | ||
| blockquote { | ||
| padding-left: 20px; | ||
| border-left: 3px solid #0066cc; | ||
| margin-top: 20px; | ||
| } | ||
| /* Node links */ | ||
@@ -179,2 +220,113 @@ .weave-node-link { | ||
| /* Stretch/Nutshell-style expandable content */ | ||
| .weave-stretch-trigger { | ||
| color: #2b67ad; | ||
| text-decoration: none; | ||
| border-bottom: 2px dotted #2b67ad; | ||
| cursor: pointer; | ||
| position: relative; | ||
| } | ||
| .weave-stretch-trigger:hover { | ||
| background: rgba(43, 103, 173, 0.1); | ||
| } | ||
| .weave-stretch-trigger.expanded { | ||
| border-bottom-style: solid; | ||
| } | ||
| .weave-stretch-bubble { | ||
| display: block; | ||
| position: relative; | ||
| margin-top: 20px; | ||
| transition: opacity 0.3s ease-out, margin 0.3s ease-out; | ||
| opacity: 0; | ||
| margin-bottom: 0; | ||
| max-height: 0; | ||
| overflow: hidden; | ||
| } | ||
| .weave-stretch-bubble.open { | ||
| max-height: none; | ||
| overflow: visible; | ||
| opacity: 1; | ||
| margin-bottom: 0.75rem; | ||
| } | ||
| .weave-stretch-content { | ||
| background: #fff; | ||
| border: 2px solid #ccc; | ||
| border-radius: 1rem; | ||
| padding: 1rem 0.8rem; | ||
| position: relative; | ||
| box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); | ||
| } | ||
| /* Arrow pointing up to trigger (Nutshell-style) */ | ||
| .weave-stretch-arrow { | ||
| width: 0; | ||
| height: 0; | ||
| border-left: 20px solid transparent; | ||
| border-right: 20px solid transparent; | ||
| border-bottom: 20px solid #ccc; | ||
| position: absolute; | ||
| top: -20px; | ||
| pointer-events: none; | ||
| } | ||
| .weave-stretch-arrow::after { | ||
| content: ''; | ||
| width: 0; | ||
| height: 0; | ||
| border-left: 20px solid transparent; | ||
| border-right: 20px solid transparent; | ||
| border-bottom: 20px solid #fff; | ||
| position: absolute; | ||
| top: 2px; | ||
| left: -20px; | ||
| pointer-events: none; | ||
| } | ||
| .weave-stretch-content p:first-child { | ||
| margin-top: 0; | ||
| } | ||
| .weave-stretch-content p:last-child { | ||
| margin-bottom: 0; | ||
| } | ||
| .weave-stretch-content ul, | ||
| .weave-stretch-content ol { | ||
| padding-left: 1.5rem; | ||
| margin: 0.5rem 0; | ||
| } | ||
| /* Nested stretch bubbles - slightly different style */ | ||
| .weave-stretch-content .weave-stretch-content { | ||
| background: #fafafa; | ||
| border-color: #ddd; | ||
| } | ||
| .weave-stretch-content .weave-stretch-arrow { | ||
| border-bottom-color: #ddd; | ||
| } | ||
| .weave-stretch-content .weave-stretch-arrow::after { | ||
| border-bottom-color: #fafafa; | ||
| } | ||
| /* Level 3+ nesting */ | ||
| .weave-stretch-content .weave-stretch-content .weave-stretch-content { | ||
| background: #f5f5f5; | ||
| border-color: #e0e0e0; | ||
| } | ||
| .weave-stretch-content .weave-stretch-content .weave-stretch-arrow { | ||
| border-bottom-color: #e0e0e0; | ||
| } | ||
| .weave-stretch-content .weave-stretch-content .weave-stretch-arrow::after { | ||
| border-bottom-color: #f5f5f5; | ||
| } | ||
| /* Footnote references */ | ||
@@ -478,2 +630,21 @@ .weave-footnote-ref { | ||
| /* Task lists */ | ||
| ul:has(input[type="checkbox"]) { | ||
| list-style: none; | ||
| padding-left: 0; | ||
| } | ||
| ul:has(input[type="checkbox"]) ul { | ||
| list-style: none; | ||
| padding-left: 1.5em; | ||
| } | ||
| li:has(> input[type="checkbox"]) { | ||
| margin-bottom: 0.25em; | ||
| } | ||
| input[type="checkbox"] { | ||
| margin-right: 0.5em; | ||
| } | ||
| /* Code blocks */ | ||
@@ -726,2 +897,55 @@ pre { | ||
| // Stretch/Nutshell-style expand/collapse handling | ||
| document.addEventListener('click', (e) => { | ||
| const trigger = e.target.closest('.weave-stretch-trigger'); | ||
| if (!trigger) return; | ||
| e.preventDefault(); | ||
| const stretchId = trigger.dataset.stretchId; | ||
| // Find or create bubble | ||
| let bubble = trigger._bubble; | ||
| if (!bubble) { | ||
| const section = sections[stretchId]; | ||
| if (!section) return; | ||
| bubble = document.createElement('div'); | ||
| bubble.className = 'weave-stretch-bubble'; | ||
| bubble.dataset.stretchId = stretchId; | ||
| bubble.innerHTML = '<div class="weave-stretch-content"><div class="weave-stretch-arrow"></div>' + section.html + '</div>'; | ||
| // Insert bubble after the parent paragraph | ||
| const paragraph = trigger.closest('p, li, td, div.weave-stretch-content'); | ||
| if (paragraph && paragraph.parentNode) { | ||
| paragraph.parentNode.insertBefore(bubble, paragraph.nextSibling); | ||
| } else { | ||
| trigger.parentNode.insertBefore(bubble, trigger.nextSibling); | ||
| } | ||
| // Position arrow to point at trigger | ||
| const arrow = bubble.querySelector('.weave-stretch-arrow'); | ||
| const triggerRect = trigger.getBoundingClientRect(); | ||
| const bubbleRect = bubble.getBoundingClientRect(); | ||
| const arrowLeft = triggerRect.left - bubbleRect.left + (triggerRect.width / 2) - 20; // -20 to center the arrow | ||
| arrow.style.left = arrowLeft + 'px'; | ||
| trigger._bubble = bubble; | ||
| // Force reflow for animation | ||
| bubble.offsetHeight; | ||
| } | ||
| // Toggle open/close | ||
| const isOpen = bubble.classList.contains('open'); | ||
| if (isOpen) { | ||
| bubble.classList.remove('open'); | ||
| trigger.classList.remove('expanded'); | ||
| } else { | ||
| bubble.classList.add('open'); | ||
| trigger.classList.add('expanded'); | ||
| } | ||
| }); | ||
| // Footnote backlink tracking - remember which reference was clicked | ||
@@ -728,0 +952,0 @@ document.addEventListener('click', (e) => { |
+12
-12
| { | ||
| "name": "@weave-md/basic", | ||
| "version": "0.3.0-alpha.0", | ||
| "version": "0.3.1-alpha.0", | ||
| "description": "Reference implementation of Weave Markdown - CLI, renderer, and exporter", | ||
@@ -20,6 +20,2 @@ "type": "module", | ||
| ], | ||
| "scripts": { | ||
| "build": "tsc", | ||
| "test": "cd ../.. && pnpm vitest run packages/basic/test" | ||
| }, | ||
| "keywords": [ | ||
@@ -40,11 +36,11 @@ "weave", | ||
| "peerDependencies": { | ||
| "@weave-md/core": "workspace:*" | ||
| "@weave-md/core": "^0.3.1-alpha.0" | ||
| }, | ||
| "dependencies": { | ||
| "@weave-md/parse": "workspace:*", | ||
| "@weave-md/validate": "workspace:*", | ||
| "hast-util-to-html": "^9.0.5", | ||
| "katex": "^0.16.9", | ||
| "mdast-util-to-hast": "^13.2.1", | ||
| "unist-util-visit": "^5.0.0" | ||
| "unist-util-visit": "^5.0.0", | ||
| "@weave-md/validate": "^0.3.1-alpha.0", | ||
| "@weave-md/parse": "^0.3.1-alpha.0" | ||
| }, | ||
@@ -55,6 +51,10 @@ "devDependencies": { | ||
| "@types/node": "^20.10.0", | ||
| "@weave-md/core": "workspace:*", | ||
| "typescript": "^5.7.0", | ||
| "vitest": "^1.6.0" | ||
| "vitest": "^1.6.0", | ||
| "@weave-md/core": "^0.3.1-alpha.0" | ||
| }, | ||
| "scripts": { | ||
| "build": "tsc", | ||
| "test": "cd ../.. && pnpm vitest run packages/basic/test" | ||
| } | ||
| } | ||
| } |
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
113305
15.35%15
7.14%2206
12.32%+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added