- Replace automatic auth controls with developer-placed .insertr-gate elements - Add OAuth-ready authentication flow with mock implementation - Support any HTML element as gate with custom styling - Implement proper gate restoration after authentication - Move auth controls to bottom-right corner for better UX - Add editor gates to demo pages (footer link and styled button) - Maintain gates visible by default with hideGatesAfterAuth option - Prevent duplicate authentication attempts with loading states This enables small business owners to access editor via discrete footer links or custom-styled elements placed anywhere by developers.
210 lines
5.6 KiB
Go
210 lines
5.6 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 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, libraryScript string) {
|
|
if !isDevelopment {
|
|
return // Only inject in development mode for now
|
|
}
|
|
|
|
// Find the head element
|
|
head := i.findHeadElement(doc)
|
|
if head == nil {
|
|
return
|
|
}
|
|
|
|
// Add inline script with embedded library
|
|
// Note: Using html.TextNode for scripts can cause issues with HTML entity encoding
|
|
// Instead, we'll insert the script tag as raw HTML
|
|
scriptHTML := fmt.Sprintf(`<script type="text/javascript">
|
|
%s
|
|
</script>`, libraryScript)
|
|
|
|
// Parse the script HTML and append to head
|
|
scriptNodes, err := html.ParseFragment(strings.NewReader(scriptHTML), head)
|
|
if err == nil && len(scriptNodes) > 0 {
|
|
for _, node := range scriptNodes {
|
|
head.AppendChild(node)
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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
|
|
}
|