Implement complete content injection and enhancement pipeline
- Add content API client with HTTP and mock implementations - Implement HTML content injection with database content replacement - Create enhance command for build-time content injection - Integrate enhancement with servedev for live development workflow - Add editor asset injection and serving (/_insertr/ endpoints) - Support on-the-fly HTML enhancement during development - Enable complete 'Tailwind of CMS' workflow: parse → inject → serve
This commit is contained in:
204
insertr-cli/pkg/content/injector.go
Normal file
204
insertr-cli/pkg/content/injector.go
Normal file
@@ -0,0 +1,204 @@
|
||||
package content
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"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 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.setAttribute(node, "data-insertr-enhanced", "true")
|
||||
}
|
||||
|
||||
// InjectEditorAssets adds editor JavaScript and CSS to HTML document
|
||||
func (i *Injector) InjectEditorAssets(doc *html.Node, isDevelopment bool) {
|
||||
if !isDevelopment {
|
||||
return // Only inject in development mode for now
|
||||
}
|
||||
|
||||
// Find the head element
|
||||
head := i.findHeadElement(doc)
|
||||
if head == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Add editor JavaScript
|
||||
script := &html.Node{
|
||||
Type: html.ElementNode,
|
||||
Data: "script",
|
||||
Attr: []html.Attribute{
|
||||
{Key: "src", Val: "/_insertr/insertr-editor.js"},
|
||||
{Key: "defer", Val: ""},
|
||||
},
|
||||
}
|
||||
head.AppendChild(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,
|
||||
})
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
Reference in New Issue
Block a user