refactor: consolidate database structure and move injector to engine
Database Structure Cleanup: - Move all SQL files from ./db/ to ./internal/db/ - Update sqlc.yaml to use new paths (preserving schema+setup.sql hack) - Consolidate database-related code in single directory - Remove empty ./db/ directory Injector Migration: - Move injector.go from content package to engine package - Update ContentClient interface to return map instead of slice for GetBulkContent - Update database client implementation to match interface - Remove injector dependency from enhancer (stub implementation) Demo-Site Consolidation: - Move demo-site to test-sites/demo-site for better organization - Update build scripts to use new demo-site location - Maintain all functionality while improving project structure This continues the unified architecture consolidation by moving core content processing logic to the engine and organizing related files properly.
This commit is contained in:
@@ -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),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,505 +0,0 @@
|
||||
package content
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/net/html"
|
||||
)
|
||||
|
||||
// Injector handles content injection into HTML elements
|
||||
type Injector struct {
|
||||
client ContentClient
|
||||
siteID string
|
||||
mdProcessor *MarkdownProcessor
|
||||
}
|
||||
|
||||
// NewInjector creates a new content injector
|
||||
func NewInjector(client ContentClient, siteID string) *Injector {
|
||||
return &Injector{
|
||||
client: client,
|
||||
siteID: siteID,
|
||||
mdProcessor: NewMarkdownProcessor(),
|
||||
}
|
||||
}
|
||||
|
||||
// InjectContent replaces element content with database values and adds content IDs
|
||||
func (i *Injector) InjectContent(element *Element, contentID string) error {
|
||||
// Fetch content from database/API
|
||||
contentItem, err := i.client.GetContent(i.siteID, contentID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("fetching content for %s: %w", contentID, err)
|
||||
}
|
||||
|
||||
// If no content found, keep original content but add data attributes
|
||||
if contentItem == nil {
|
||||
i.AddContentAttributes(element.Node, contentID, element.Type)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Replace element content based on type
|
||||
switch element.Type {
|
||||
case "text":
|
||||
i.injectTextContent(element.Node, contentItem.Value)
|
||||
case "markdown":
|
||||
i.injectMarkdownContent(element.Node, contentItem.Value)
|
||||
case "link":
|
||||
i.injectLinkContent(element.Node, contentItem.Value)
|
||||
default:
|
||||
i.injectTextContent(element.Node, contentItem.Value)
|
||||
}
|
||||
|
||||
// Add data attributes for editor functionality
|
||||
i.AddContentAttributes(element.Node, contentID, element.Type)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// InjectBulkContent efficiently injects multiple content items
|
||||
func (i *Injector) InjectBulkContent(elements []ElementWithID) error {
|
||||
// Extract content IDs for bulk fetch
|
||||
contentIDs := make([]string, len(elements))
|
||||
for idx, elem := range elements {
|
||||
contentIDs[idx] = elem.ContentID
|
||||
}
|
||||
|
||||
// Bulk fetch content
|
||||
contentMap, err := i.client.GetBulkContent(i.siteID, contentIDs)
|
||||
if err != nil {
|
||||
return fmt.Errorf("bulk fetching content: %w", err)
|
||||
}
|
||||
|
||||
// Inject each element
|
||||
for _, elem := range elements {
|
||||
contentItem, exists := contentMap[elem.ContentID]
|
||||
|
||||
// Add content attributes regardless
|
||||
i.AddContentAttributes(elem.Element.Node, elem.ContentID, elem.Element.Type)
|
||||
|
||||
if !exists {
|
||||
// Keep original content if not found in database
|
||||
continue
|
||||
}
|
||||
|
||||
// Replace content based on type
|
||||
switch elem.Element.Type {
|
||||
case "text":
|
||||
i.injectTextContent(elem.Element.Node, contentItem.Value)
|
||||
case "markdown":
|
||||
i.injectMarkdownContent(elem.Element.Node, contentItem.Value)
|
||||
case "link":
|
||||
i.injectLinkContent(elem.Element.Node, contentItem.Value)
|
||||
default:
|
||||
i.injectTextContent(elem.Element.Node, contentItem.Value)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// injectTextContent replaces text content in an element
|
||||
func (i *Injector) injectTextContent(node *html.Node, content string) {
|
||||
// Remove all child nodes
|
||||
for child := node.FirstChild; child != nil; {
|
||||
next := child.NextSibling
|
||||
node.RemoveChild(child)
|
||||
child = next
|
||||
}
|
||||
|
||||
// Add new text content
|
||||
textNode := &html.Node{
|
||||
Type: html.TextNode,
|
||||
Data: content,
|
||||
}
|
||||
node.AppendChild(textNode)
|
||||
}
|
||||
|
||||
// injectMarkdownContent handles markdown content - converts markdown to HTML
|
||||
func (i *Injector) injectMarkdownContent(node *html.Node, content string) {
|
||||
if content == "" {
|
||||
i.injectTextContent(node, "")
|
||||
return
|
||||
}
|
||||
|
||||
// Convert markdown to HTML using server processor
|
||||
htmlContent, err := i.mdProcessor.ToHTML(content)
|
||||
if err != nil {
|
||||
log.Printf("⚠️ Markdown conversion failed for content '%s': %v, falling back to text", content, err)
|
||||
i.injectTextContent(node, content)
|
||||
return
|
||||
}
|
||||
|
||||
// Inject the HTML content
|
||||
i.injectHTMLContent(node, htmlContent)
|
||||
}
|
||||
|
||||
// injectLinkContent handles link/button content with URL extraction
|
||||
func (i *Injector) injectLinkContent(node *html.Node, content string) {
|
||||
// For now, just inject the text content
|
||||
// TODO: Parse content for URL and text components
|
||||
i.injectTextContent(node, content)
|
||||
}
|
||||
|
||||
// injectHTMLContent safely injects HTML content into a DOM node
|
||||
// Preserves the original element and only replaces its content
|
||||
func (i *Injector) injectHTMLContent(node *html.Node, htmlContent string) {
|
||||
// Clear existing content but preserve the element itself
|
||||
i.clearNode(node)
|
||||
|
||||
if htmlContent == "" {
|
||||
return
|
||||
}
|
||||
|
||||
// Wrap content for safe parsing
|
||||
wrappedHTML := "<div>" + htmlContent + "</div>"
|
||||
|
||||
// Parse HTML string
|
||||
doc, err := html.Parse(strings.NewReader(wrappedHTML))
|
||||
if err != nil {
|
||||
log.Printf("Failed to parse HTML content '%s': %v, falling back to text", htmlContent, err)
|
||||
i.injectTextContent(node, htmlContent)
|
||||
return
|
||||
}
|
||||
|
||||
// Find the wrapper div and move its children to target node
|
||||
wrapper := i.findElementByTag(doc, "div")
|
||||
if wrapper == nil {
|
||||
log.Printf("Could not find wrapper div in parsed HTML")
|
||||
return
|
||||
}
|
||||
|
||||
// Move parsed nodes to target element (preserving original element)
|
||||
for child := wrapper.FirstChild; child != nil; {
|
||||
next := child.NextSibling
|
||||
wrapper.RemoveChild(child)
|
||||
node.AppendChild(child)
|
||||
child = next
|
||||
}
|
||||
}
|
||||
|
||||
// clearNode removes all child nodes from a given node
|
||||
func (i *Injector) clearNode(node *html.Node) {
|
||||
for child := node.FirstChild; child != nil; {
|
||||
next := child.NextSibling
|
||||
node.RemoveChild(child)
|
||||
child = next
|
||||
}
|
||||
}
|
||||
|
||||
// findElementByTag finds the first element with the specified tag name
|
||||
func (i *Injector) findElementByTag(node *html.Node, tag string) *html.Node {
|
||||
if node.Type == html.ElementNode && node.Data == tag {
|
||||
return node
|
||||
}
|
||||
|
||||
for child := node.FirstChild; child != nil; child = child.NextSibling {
|
||||
if found := i.findElementByTag(child, tag); found != nil {
|
||||
return found
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// AddContentAttributes adds necessary data attributes and insertr class for editor functionality
|
||||
func (i *Injector) AddContentAttributes(node *html.Node, contentID string, contentType string) {
|
||||
i.setAttribute(node, "data-content-id", contentID)
|
||||
i.setAttribute(node, "data-content-type", contentType)
|
||||
i.addClass(node, "insertr")
|
||||
}
|
||||
|
||||
// InjectEditorAssets adds editor JavaScript to HTML document and injects demo gate if needed
|
||||
func (i *Injector) InjectEditorAssets(doc *html.Node, isDevelopment bool, libraryScript string) {
|
||||
// Inject demo gate if no gates exist and add script for functionality
|
||||
if isDevelopment {
|
||||
i.InjectDemoGateIfNeeded(doc)
|
||||
i.InjectEditorScript(doc)
|
||||
}
|
||||
|
||||
// TODO: Implement CDN script injection for production
|
||||
// Production options:
|
||||
// 1. Inject CDN script tag: <script src="https://cdn.jsdelivr.net/npm/@insertr/lib@1.0.0/dist/insertr.js"></script>
|
||||
}
|
||||
|
||||
// findHeadElement finds the <head> element in the document
|
||||
func (i *Injector) findHeadElement(node *html.Node) *html.Node {
|
||||
if node.Type == html.ElementNode && node.Data == "head" {
|
||||
return node
|
||||
}
|
||||
|
||||
for child := node.FirstChild; child != nil; child = child.NextSibling {
|
||||
if result := i.findHeadElement(child); result != nil {
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// setAttribute safely sets an attribute on an HTML node
|
||||
func (i *Injector) setAttribute(node *html.Node, key, value string) {
|
||||
// Remove existing attribute if present
|
||||
for idx, attr := range node.Attr {
|
||||
if attr.Key == key {
|
||||
node.Attr = append(node.Attr[:idx], node.Attr[idx+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Add new attribute
|
||||
node.Attr = append(node.Attr, html.Attribute{
|
||||
Key: key,
|
||||
Val: value,
|
||||
})
|
||||
}
|
||||
|
||||
// addClass safely adds a class to an HTML node
|
||||
func (i *Injector) addClass(node *html.Node, className string) {
|
||||
var classAttr *html.Attribute
|
||||
var classIndex int = -1
|
||||
|
||||
// Find existing class attribute
|
||||
for idx, attr := range node.Attr {
|
||||
if attr.Key == "class" {
|
||||
classAttr = &attr
|
||||
classIndex = idx
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
var classes []string
|
||||
if classAttr != nil {
|
||||
classes = strings.Fields(classAttr.Val)
|
||||
}
|
||||
|
||||
// Check if class already exists
|
||||
for _, class := range classes {
|
||||
if class == className {
|
||||
return // Class already exists
|
||||
}
|
||||
}
|
||||
|
||||
// Add new class
|
||||
classes = append(classes, className)
|
||||
newClassValue := strings.Join(classes, " ")
|
||||
|
||||
if classIndex >= 0 {
|
||||
// Update existing class attribute
|
||||
node.Attr[classIndex].Val = newClassValue
|
||||
} else {
|
||||
// Add new class attribute
|
||||
node.Attr = append(node.Attr, html.Attribute{
|
||||
Key: "class",
|
||||
Val: newClassValue,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Element represents a parsed HTML element with metadata
|
||||
type Element struct {
|
||||
Node *html.Node
|
||||
Type string
|
||||
Tag string
|
||||
Classes []string
|
||||
Content string
|
||||
}
|
||||
|
||||
// ElementWithID combines an element with its generated content ID
|
||||
type ElementWithID struct {
|
||||
Element *Element
|
||||
ContentID string
|
||||
}
|
||||
|
||||
// InjectDemoGateIfNeeded injects a demo gate element if no .insertr-gate elements exist
|
||||
func (i *Injector) InjectDemoGateIfNeeded(doc *html.Node) {
|
||||
// Check if any .insertr-gate elements already exist
|
||||
if i.hasInsertrGate(doc) {
|
||||
return
|
||||
}
|
||||
|
||||
// Find the body element
|
||||
bodyNode := i.findBodyElement(doc)
|
||||
if bodyNode == nil {
|
||||
log.Printf("Warning: Could not find body element to inject demo gate")
|
||||
return
|
||||
}
|
||||
|
||||
// Create demo gate HTML structure
|
||||
gateHTML := `<div class="insertr-demo-gate" style="position: fixed; top: 20px; right: 20px; z-index: 9999; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;">
|
||||
<button class="insertr-gate insertr-demo-gate-btn" style="background: #4f46e5; color: white; border: none; padding: 10px 16px; border-radius: 8px; font-size: 14px; font-weight: 500; cursor: pointer; box-shadow: 0 4px 12px rgba(79, 70, 229, 0.3); transition: all 0.2s ease; display: flex; align-items: center; gap: 8px; user-select: none;" onmouseover="this.style.background='#4338ca'; this.style.transform='translateY(-2px)'; this.style.boxShadow='0 6px 16px rgba(79, 70, 229, 0.4)'" onmouseout="this.style.background='#4f46e5'; this.style.transform='translateY(0)'; this.style.boxShadow='0 4px 12px rgba(79, 70, 229, 0.3)'">
|
||||
<span style="font-size: 16px;">✏️</span>
|
||||
<span>Edit Site</span>
|
||||
</button>
|
||||
</div>`
|
||||
|
||||
// Parse the gate HTML and inject it into the body
|
||||
gateDoc, err := html.Parse(strings.NewReader(gateHTML))
|
||||
if err != nil {
|
||||
log.Printf("Error parsing demo gate HTML: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Extract and inject the gate element
|
||||
if gateDiv := i.extractElementByClass(gateDoc, "insertr-demo-gate"); gateDiv != nil {
|
||||
if gateDiv.Parent != nil {
|
||||
gateDiv.Parent.RemoveChild(gateDiv)
|
||||
}
|
||||
bodyNode.AppendChild(gateDiv)
|
||||
log.Printf("✅ Demo gate injected: Edit button added to top-right corner")
|
||||
}
|
||||
}
|
||||
|
||||
// InjectEditorScript injects the insertr.js library and initialization script
|
||||
func (i *Injector) InjectEditorScript(doc *html.Node) {
|
||||
// Find the head element for the script tag
|
||||
headNode := i.findHeadElement(doc)
|
||||
if headNode == nil {
|
||||
log.Printf("Warning: Could not find head element to inject editor script")
|
||||
return
|
||||
}
|
||||
|
||||
// Create script element that loads insertr.js from our server
|
||||
scriptHTML := fmt.Sprintf(`<script src="http://localhost:8080/insertr.js"></script>
|
||||
<script type="text/javascript">
|
||||
// Initialize insertr for demo sites
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
if (typeof window.Insertr !== 'undefined') {
|
||||
console.log('✅ Insertr library loaded successfully');
|
||||
|
||||
// The library has auto-initialization, but we can force initialization
|
||||
// with our demo configuration
|
||||
window.Insertr.init({
|
||||
siteId: '%s',
|
||||
apiEndpoint: 'http://localhost:8080/api/content',
|
||||
mockAuth: true, // Use mock authentication for demos
|
||||
debug: true
|
||||
});
|
||||
|
||||
console.log('✅ Insertr initialized for demo site with config:', {
|
||||
siteId: '%s',
|
||||
apiEndpoint: 'http://localhost:8080/api/content',
|
||||
mockAuth: true
|
||||
});
|
||||
} else {
|
||||
console.error('❌ Insertr library failed to load');
|
||||
|
||||
// Fallback for demo gates if library fails
|
||||
const gates = document.querySelectorAll('.insertr-gate');
|
||||
gates.forEach(gate => {
|
||||
gate.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
alert('🚧 Insertr library not loaded\\n\\nPlease run "just build-lib" to build the library first.');
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>`, i.siteID, i.siteID)
|
||||
|
||||
// Parse and inject the script
|
||||
scriptDoc, err := html.Parse(strings.NewReader(scriptHTML))
|
||||
if err != nil {
|
||||
log.Printf("Error parsing editor script HTML: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Extract and inject all script elements
|
||||
if err := i.injectAllScriptElements(scriptDoc, headNode); err != nil {
|
||||
log.Printf("Error injecting script elements: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("✅ Insertr.js library and initialization script injected")
|
||||
}
|
||||
|
||||
// injectAllScriptElements finds and injects all script elements from parsed HTML
|
||||
func (i *Injector) injectAllScriptElements(doc *html.Node, targetNode *html.Node) error {
|
||||
scripts := i.findAllScriptElements(doc)
|
||||
|
||||
for _, script := range scripts {
|
||||
// Remove from original parent
|
||||
if script.Parent != nil {
|
||||
script.Parent.RemoveChild(script)
|
||||
}
|
||||
// Add to target node
|
||||
targetNode.AppendChild(script)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// findAllScriptElements recursively finds all script elements
|
||||
func (i *Injector) findAllScriptElements(node *html.Node) []*html.Node {
|
||||
var scripts []*html.Node
|
||||
|
||||
if node.Type == html.ElementNode && node.Data == "script" {
|
||||
scripts = append(scripts, node)
|
||||
}
|
||||
|
||||
for child := node.FirstChild; child != nil; child = child.NextSibling {
|
||||
childScripts := i.findAllScriptElements(child)
|
||||
scripts = append(scripts, childScripts...)
|
||||
}
|
||||
|
||||
return scripts
|
||||
}
|
||||
|
||||
// hasInsertrGate checks if document has .insertr-gate elements
|
||||
func (i *Injector) hasInsertrGate(node *html.Node) bool {
|
||||
if node.Type == html.ElementNode {
|
||||
for _, attr := range node.Attr {
|
||||
if attr.Key == "class" && strings.Contains(attr.Val, "insertr-gate") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
for child := node.FirstChild; child != nil; child = child.NextSibling {
|
||||
if i.hasInsertrGate(child) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// findBodyElement finds the <body> element
|
||||
func (i *Injector) findBodyElement(node *html.Node) *html.Node {
|
||||
if node.Type == html.ElementNode && node.Data == "body" {
|
||||
return node
|
||||
}
|
||||
for child := node.FirstChild; child != nil; child = child.NextSibling {
|
||||
if result := i.findBodyElement(child); result != nil {
|
||||
return result
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// extractElementByClass finds element with specific class
|
||||
func (i *Injector) extractElementByClass(node *html.Node, className string) *html.Node {
|
||||
if node.Type == html.ElementNode {
|
||||
for _, attr := range node.Attr {
|
||||
if attr.Key == "class" && strings.Contains(attr.Val, className) {
|
||||
return node
|
||||
}
|
||||
}
|
||||
}
|
||||
for child := node.FirstChild; child != nil; child = child.NextSibling {
|
||||
if result := i.extractElementByClass(child, className); result != nil {
|
||||
return result
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// extractElementByTag finds element with specific tag
|
||||
func (i *Injector) extractElementByTag(node *html.Node, tagName string) *html.Node {
|
||||
if node.Type == html.ElementNode && node.Data == tagName {
|
||||
return node
|
||||
}
|
||||
for child := node.FirstChild; child != nil; child = child.NextSibling {
|
||||
if result := i.extractElementByTag(child, tagName); result != nil {
|
||||
return result
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user