Files
insertr/insertr-cli/pkg/content/injector.go
Joakim ae9d8e4058 refactor: implement script tag approach for library inclusion
- Add script tags to demo-site HTML files for manual development
- Disable CLI inline script injection to prevent duplicate scripts
- Add library serving endpoints to servedev command
- Update build process to auto-copy library to demo-site
- Add CDN URL helpers for future production deployment
- Update .gitignore for generated demo-site files

Fixes .insertr-gate authentication for manual npm run serve workflow
while maintaining clean separation between CLI and manual setups.
2025-09-07 18:28:34 +02:00

237 lines
6.3 KiB
Go

package content
import (
"fmt"
"strings"
"golang.org/x/net/html"
)
// Injector handles content injection into HTML elements
type Injector struct {
client ContentClient
siteID string
}
// NewInjector creates a new content injector
func NewInjector(client ContentClient, siteID string) *Injector {
return &Injector{
client: client,
siteID: siteID,
}
}
// 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 (for now, just as text)
func (i *Injector) injectMarkdownContent(node *html.Node, content string) {
// For now, treat markdown as text content
// TODO: Implement markdown to HTML conversion
i.injectTextContent(node, content)
}
// 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)
}
// 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
func (i *Injector) InjectEditorAssets(doc *html.Node, isDevelopment bool, libraryScript string) {
// TODO: Implement script injection strategy when we have CDN hosting
// For now, script injection is disabled since HTML files should include their own script tags
// Future options:
// 1. Inject CDN script tag: <script src="https://cdn.jsdelivr.net/npm/@insertr/lib@1.0.0/dist/insertr.js"></script>
// 2. Inject local script tag for development: <script src="/insertr/insertr.js"></script>
// 3. Continue with inline injection for certain use cases
// Currently disabled to avoid duplicate scripts
return
}
// 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
}