- Rebuild JavaScript library with delayed control panel initialization - Update server assets to include latest UI behavior changes - Ensure built assets reflect invisible UI for regular visitors The control panel now only appears after gate activation, maintaining the invisible CMS principle for end users.
239 lines
6.2 KiB
Go
239 lines
6.2 KiB
Go
package engine
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
|
|
"golang.org/x/net/html"
|
|
)
|
|
|
|
// AuthProvider represents authentication provider information
|
|
type AuthProvider struct {
|
|
Type string // "mock", "jwt", "authentik"
|
|
}
|
|
|
|
// ContentEngine is the unified content processing engine
|
|
type ContentEngine struct {
|
|
idGenerator *IDGenerator
|
|
client ContentClient
|
|
authProvider *AuthProvider
|
|
}
|
|
|
|
// NewContentEngine creates a new content processing engine
|
|
func NewContentEngine(client ContentClient) *ContentEngine {
|
|
return &ContentEngine{
|
|
idGenerator: NewIDGenerator(),
|
|
client: client,
|
|
authProvider: &AuthProvider{Type: "mock"}, // default
|
|
}
|
|
}
|
|
|
|
// NewContentEngineWithAuth creates a new content processing engine with auth config
|
|
func NewContentEngineWithAuth(client ContentClient, authProvider *AuthProvider) *ContentEngine {
|
|
if authProvider == nil {
|
|
authProvider = &AuthProvider{Type: "mock"}
|
|
}
|
|
return &ContentEngine{
|
|
idGenerator: NewIDGenerator(),
|
|
client: client,
|
|
authProvider: authProvider,
|
|
}
|
|
}
|
|
|
|
// ProcessContent processes HTML content according to the specified mode
|
|
func (e *ContentEngine) ProcessContent(input ContentInput) (*ContentResult, error) {
|
|
// 1. Parse HTML
|
|
doc, err := html.Parse(strings.NewReader(string(input.HTML)))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("parsing HTML: %w", err)
|
|
}
|
|
|
|
// 2. Find insertr elements
|
|
elements := e.findInsertrElements(doc)
|
|
|
|
// 3. Generate IDs for elements
|
|
generatedIDs := make(map[string]string)
|
|
processedElements := make([]ProcessedElement, len(elements))
|
|
|
|
for i, elem := range elements {
|
|
// Check if element already has a data-content-id
|
|
existingID := e.getAttribute(elem.Node, "data-content-id")
|
|
var id string
|
|
var wasGenerated bool
|
|
|
|
if existingID != "" {
|
|
// Use existing ID from enhanced element
|
|
id = existingID
|
|
wasGenerated = false
|
|
} else {
|
|
// Generate new ID for unprocessed element
|
|
id = e.idGenerator.Generate(elem.Node, input.FilePath)
|
|
wasGenerated = true
|
|
}
|
|
|
|
generatedIDs[fmt.Sprintf("element_%d", i)] = id
|
|
|
|
processedElements[i] = ProcessedElement{
|
|
Node: elem.Node,
|
|
ID: id,
|
|
Type: elem.Type,
|
|
Generated: wasGenerated,
|
|
Tag: elem.Node.Data,
|
|
Classes: GetClasses(elem.Node),
|
|
}
|
|
|
|
// Add/update content attributes to the node
|
|
e.addContentAttributes(elem.Node, id, elem.Type)
|
|
}
|
|
|
|
// 4. Inject content if required by mode
|
|
if input.Mode == Enhancement || input.Mode == ContentInjection {
|
|
err = e.injectContent(processedElements, input.SiteID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("injecting content: %w", err)
|
|
}
|
|
}
|
|
|
|
// 5. Inject editor assets for enhancement mode (development)
|
|
if input.Mode == Enhancement {
|
|
injector := NewInjectorWithAuth(e.client, input.SiteID, e.authProvider)
|
|
injector.InjectEditorAssets(doc, true, "")
|
|
}
|
|
|
|
return &ContentResult{
|
|
Document: doc,
|
|
Elements: processedElements,
|
|
GeneratedIDs: generatedIDs,
|
|
}, nil
|
|
}
|
|
|
|
// InsertrElement represents an insertr element found in HTML
|
|
type InsertrElement struct {
|
|
Node *html.Node
|
|
Type string
|
|
}
|
|
|
|
// findInsertrElements finds all elements with class="insertr"
|
|
func (e *ContentEngine) findInsertrElements(doc *html.Node) []InsertrElement {
|
|
var elements []InsertrElement
|
|
e.walkNodes(doc, func(n *html.Node) {
|
|
if n.Type == html.ElementNode && e.hasInsertrClass(n) {
|
|
elementType := e.determineContentType(n)
|
|
elements = append(elements, InsertrElement{
|
|
Node: n,
|
|
Type: elementType,
|
|
})
|
|
}
|
|
})
|
|
return elements
|
|
}
|
|
|
|
// walkNodes walks through all nodes in the document
|
|
func (e *ContentEngine) walkNodes(n *html.Node, fn func(*html.Node)) {
|
|
fn(n)
|
|
for c := n.FirstChild; c != nil; c = c.NextSibling {
|
|
e.walkNodes(c, fn)
|
|
}
|
|
}
|
|
|
|
// hasInsertrClass checks if node has class="insertr"
|
|
func (e *ContentEngine) hasInsertrClass(node *html.Node) bool {
|
|
classes := GetClasses(node)
|
|
for _, class := range classes {
|
|
if class == "insertr" {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// determineContentType determines the content type based on element
|
|
func (e *ContentEngine) determineContentType(node *html.Node) string {
|
|
tag := strings.ToLower(node.Data)
|
|
|
|
switch tag {
|
|
case "a", "button":
|
|
return "link"
|
|
case "h1", "h2", "h3", "h4", "h5", "h6":
|
|
return "text"
|
|
case "p", "div", "section", "article", "span":
|
|
return "markdown"
|
|
default:
|
|
return "text"
|
|
}
|
|
}
|
|
|
|
// addContentAttributes adds data-content-id and data-content-type attributes
|
|
func (e *ContentEngine) addContentAttributes(node *html.Node, contentID, contentType string) {
|
|
// Add data-content-id attribute
|
|
e.setAttribute(node, "data-content-id", contentID)
|
|
// Add data-content-type attribute
|
|
e.setAttribute(node, "data-content-type", contentType)
|
|
}
|
|
|
|
// getAttribute gets an attribute value from an HTML node
|
|
func (e *ContentEngine) getAttribute(node *html.Node, key string) string {
|
|
for _, attr := range node.Attr {
|
|
if attr.Key == key {
|
|
return attr.Val
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// setAttribute sets an attribute on an HTML node
|
|
func (e *ContentEngine) setAttribute(node *html.Node, key, value string) {
|
|
// Remove existing attribute if it exists
|
|
for i, attr := range node.Attr {
|
|
if attr.Key == key {
|
|
node.Attr[i].Val = value
|
|
return
|
|
}
|
|
}
|
|
// Add new attribute
|
|
node.Attr = append(node.Attr, html.Attribute{
|
|
Key: key,
|
|
Val: value,
|
|
})
|
|
}
|
|
|
|
// injectContent injects content from database into elements
|
|
func (e *ContentEngine) injectContent(elements []ProcessedElement, siteID string) error {
|
|
for i := range elements {
|
|
elem := &elements[i]
|
|
|
|
// Try to get content from database
|
|
contentItem, err := e.client.GetContent(siteID, elem.ID)
|
|
if err != nil {
|
|
// Content not found is not an error - element just won't have injected content
|
|
continue
|
|
}
|
|
|
|
if contentItem != nil {
|
|
// Inject the content into the element
|
|
elem.Content = contentItem.Value
|
|
e.injectContentIntoNode(elem.Node, contentItem.Value, contentItem.Type)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// injectContentIntoNode injects content value into an HTML node
|
|
func (e *ContentEngine) injectContentIntoNode(node *html.Node, content, contentType string) {
|
|
// Clear existing text content
|
|
for child := node.FirstChild; child != nil; {
|
|
next := child.NextSibling
|
|
if child.Type == html.TextNode {
|
|
node.RemoveChild(child)
|
|
}
|
|
child = next
|
|
}
|
|
|
|
// Add new text content
|
|
textNode := &html.Node{
|
|
Type: html.TextNode,
|
|
Data: content,
|
|
}
|
|
node.AppendChild(textNode)
|
|
}
|