Files
DFTE/src/DeviceFrameworkTemplateRenderer.cpp
T
alex 73fdd28f74 Harden DFTE rendering and placeholder registration to fail safely under stalled and invalid inputs.
Add regression coverage and docs updates so these edge cases stay stable across board test runs.
2026-04-16 13:48:25 +10:00

900 lines
36 KiB
C++

#include "DeviceFrameworkTemplateRenderer.h"
#include "DeviceFrameworkTemplateEngineDebug.h"
#include <pgmspace.h>
#include <cstring>
namespace {
static bool pushPlaceholderEntry(DeviceFrameworkTemplateContext& ctx, const PlaceholderEntry* entry, const char* nameOverride = nullptr) {
if (entry == nullptr) {
DFTE_LOG_ERROR("Attempted to push null placeholder entry");
return false;
}
const char* name = nameOverride ? nameOverride : entry->name;
switch (entry->type) {
case PlaceholderType::PROGMEM_DATA:
case PlaceholderType::RAM_DATA: {
if (!ctx.pushContext(RenderingContextType::PLACEHOLDER_DATA, name)) {
return false;
}
RenderingContext* dataCtx = ctx.getCurrentContext();
dataCtx->context.data.entry = entry;
dataCtx->context.data.offset = 0;
return true;
}
case PlaceholderType::PROGMEM_TEMPLATE: {
if (!ctx.pushContext(RenderingContextType::PLACEHOLDER_TEMPLATE, name)) {
return false;
}
RenderingContext* placeholderCtx = ctx.getCurrentContext();
placeholderCtx->context.templatePlaceholder.entry = entry;
if (!ctx.pushContext(RenderingContextType::TEMPLATE, name)) {
ctx.popContext();
return false;
}
RenderingContext* templateCtx = ctx.getCurrentContext();
templateCtx->context.templateCtx.templateData = static_cast<const char*>(entry->data);
templateCtx->context.templateCtx.templateLen = entry->getLength(entry->data);
templateCtx->context.templateCtx.isProgmem = true;
templateCtx->context.templateCtx.position = 0;
templateCtx->context.templateCtx.iteratorPlaceholders = nullptr;
templateCtx->context.templateCtx.iteratorPlaceholderCount = 0;
return true;
}
case PlaceholderType::DYNAMIC_TEMPLATE: {
if (!ctx.pushContext(RenderingContextType::PLACEHOLDER_DYNAMIC_TEMPLATE, name)) {
return false;
}
RenderingContext* dynamicCtx = ctx.getCurrentContext();
dynamicCtx->context.dynamicTemplate.entry = entry;
dynamicCtx->context.dynamicTemplate.offset = 0;
dynamicCtx->context.dynamicTemplate.templateData = nullptr;
dynamicCtx->context.dynamicTemplate.templateLength = 0;
const DynamicTemplateDescriptor* descriptor = static_cast<const DynamicTemplateDescriptor*>(entry->data);
if (descriptor == nullptr || descriptor->getter == nullptr) {
DFTE_LOG_ERROR("Dynamic template placeholder missing descriptor: " + String(name));
ctx.popContext();
return false;
}
const char* templateData = descriptor->getter(descriptor->userData);
if (templateData == nullptr) {
DFTE_LOG_WARN("Dynamic template getter returned null for placeholder: " + String(name));
templateData = "";
}
size_t templateLen = DeviceFrameworkPlaceholderRegistry::getDynamicTemplateLength(descriptor, templateData);
dynamicCtx->context.dynamicTemplate.templateData = templateData;
dynamicCtx->context.dynamicTemplate.templateLength = templateLen;
if (!ctx.pushContext(RenderingContextType::TEMPLATE, name)) {
ctx.popContext();
return false;
}
RenderingContext* templateCtx = ctx.getCurrentContext();
templateCtx->context.templateCtx.templateData = templateData;
templateCtx->context.templateCtx.templateLen = templateLen;
templateCtx->context.templateCtx.isProgmem = false;
templateCtx->context.templateCtx.position = 0;
templateCtx->context.templateCtx.iteratorPlaceholders = nullptr;
templateCtx->context.templateCtx.iteratorPlaceholderCount = 0;
return true;
}
case PlaceholderType::CONDITIONAL: {
const ConditionalDescriptor* descriptor = static_cast<const ConditionalDescriptor*>(entry->data);
if (descriptor == nullptr || descriptor->evaluate == nullptr) {
DFTE_LOG_ERROR("Conditional placeholder missing descriptor: " + String(name));
return false;
}
if (!ctx.pushContext(RenderingContextType::PLACEHOLDER_CONDITIONAL, name)) {
return false;
}
RenderingContext* conditionalCtx = ctx.getCurrentContext();
conditionalCtx->context.conditional.entry = entry;
conditionalCtx->context.conditional.descriptor = descriptor;
conditionalCtx->context.conditional.branchResolved = false;
conditionalCtx->context.conditional.delegateName = nullptr;
conditionalCtx->context.conditional.delegateEntry = nullptr;
if (!ctx.registry) {
conditionalCtx->context.conditional.branchResolved = true;
return true;
}
ConditionalBranchResult branch = descriptor->evaluate(descriptor->userData);
const char* delegateName = nullptr;
switch (branch) {
case ConditionalBranchResult::TRUE_BRANCH:
delegateName = descriptor->truePlaceholder;
break;
case ConditionalBranchResult::FALSE_BRANCH:
delegateName = descriptor->falsePlaceholder;
break;
case ConditionalBranchResult::SKIP:
default:
delegateName = nullptr;
break;
}
conditionalCtx->context.conditional.branchResolved = true;
conditionalCtx->context.conditional.delegateName = delegateName;
if (delegateName == nullptr) {
return true;
}
const PlaceholderEntry* delegateEntry = ctx.registry->getPlaceholder(delegateName);
if (!delegateEntry) {
DFTE_LOG_WARN("Conditional placeholder '" + String(name) + "' referenced unknown placeholder: " + String(delegateName));
return true;
}
conditionalCtx->context.conditional.delegateEntry = delegateEntry;
if (!pushPlaceholderEntry(ctx, delegateEntry, delegateName)) {
ctx.popContext();
return false;
}
return true;
}
case PlaceholderType::ITERATOR: {
if (!ctx.pushContext(RenderingContextType::PLACEHOLDER_ITERATOR, name)) {
return false;
}
RenderingContext* iteratorCtx = ctx.getCurrentContext();
iteratorCtx->context.iterator.entry = entry;
iteratorCtx->context.iterator.descriptor = static_cast<const IteratorDescriptor*>(entry->data);
iteratorCtx->context.iterator.handle = nullptr;
iteratorCtx->context.iterator.initialized = false;
iteratorCtx->context.iterator.handleOpen = false;
const IteratorDescriptor* descriptor = iteratorCtx->context.iterator.descriptor;
if (!descriptor || !descriptor->next) {
DFTE_LOG_ERROR("Iterator placeholder missing descriptor: " + String(name));
ctx.popContext();
return false;
}
return true;
}
default:
DFTE_LOG_WARN("pushPlaceholderEntry does not support placeholder type for: " + String(name));
return false;
}
}
static DeviceFrameworkTemplateRenderer::RenderOutcome processIteratorContext(DeviceFrameworkTemplateContext& ctx, RenderingContext* iteratorCtx) {
if (!iteratorCtx) {
return DeviceFrameworkTemplateRenderer::makeError();
}
const IteratorDescriptor* descriptor = iteratorCtx->context.iterator.descriptor;
if (!descriptor || !descriptor->next) {
DFTE_LOG_ERROR("Iterator placeholder missing descriptor or next handler");
return DeviceFrameworkTemplateRenderer::makeError();
}
auto& iteratorState = iteratorCtx->context.iterator;
if (!iteratorState.initialized) {
iteratorState.handle = descriptor->open ? descriptor->open(descriptor->userData) : descriptor->userData;
iteratorState.initialized = true;
iteratorState.handleOpen = descriptor->close != nullptr && iteratorState.handle != nullptr && descriptor->open != nullptr;
}
IteratorItemView view = {};
IteratorStepResult step = descriptor->next(iteratorState.handle, view);
switch (step) {
case IteratorStepResult::ITEM_READY: {
if (!view.templateData && view.templateLength == 0) {
return DeviceFrameworkTemplateRenderer::makeState(TemplateRenderState::RENDERING_CONTEXT, true);
}
const char* templatePtr = view.templateData;
if (!templatePtr) {
return DeviceFrameworkTemplateRenderer::makeState(TemplateRenderState::RENDERING_CONTEXT, true);
}
size_t templateLen = view.templateLength;
if (templateLen == 0) {
templateLen = view.templateIsProgmem ? strlen_P(templatePtr) : strlen(templatePtr);
}
if (!ctx.pushContext(RenderingContextType::TEMPLATE, iteratorCtx->name)) {
return DeviceFrameworkTemplateRenderer::makeError();
}
RenderingContext* templateCtx = ctx.getCurrentContext();
templateCtx->context.templateCtx.templateData = templatePtr;
templateCtx->context.templateCtx.templateLen = templateLen;
templateCtx->context.templateCtx.isProgmem = view.templateIsProgmem;
templateCtx->context.templateCtx.position = 0;
templateCtx->context.templateCtx.iteratorPlaceholders = view.placeholders;
templateCtx->context.templateCtx.iteratorPlaceholderCount = view.placeholderCount;
return DeviceFrameworkTemplateRenderer::makeState(TemplateRenderState::TEXT, true);
}
case IteratorStepResult::COMPLETE: {
if (iteratorState.handleOpen && descriptor->close) {
descriptor->close(iteratorState.handle);
iteratorState.handleOpen = false;
}
iteratorState.handle = nullptr;
auto outcome = DeviceFrameworkTemplateRenderer::makeState(TemplateRenderState::RENDERING_CONTEXT, true);
outcome.popCount = 1;
RenderingContext* parent = (ctx.renderingDepth > 1) ? ctx.getContext(ctx.renderingDepth - 2) : nullptr;
if (!parent) {
outcome.nextState = TemplateRenderState::COMPLETE;
outcome.repeat = false;
outcome.finished = true;
} else if (parent->type == RenderingContextType::TEMPLATE) {
outcome.nextState = TemplateRenderState::TEXT;
}
return outcome;
}
case IteratorStepResult::ERROR:
default:
DFTE_LOG_ERROR("Iterator placeholder reported error");
if (descriptor->close && (iteratorState.handleOpen || iteratorState.handle != nullptr)) {
descriptor->close(iteratorState.handle);
}
iteratorState.handleOpen = false;
iteratorState.handle = nullptr;
return DeviceFrameworkTemplateRenderer::makeError();
}
}
} // namespace
// Helper function to convert RenderingContextType to string
static String getContextTypeString(RenderingContextType type) {
switch (type) {
case RenderingContextType::TEMPLATE: return "TEMPLATE";
case RenderingContextType::PLACEHOLDER_DATA: return "PLACEHOLDER_DATA";
case RenderingContextType::PLACEHOLDER_TEMPLATE: return "PLACEHOLDER_TEMPLATE";
default: return "UNKNOWN";
}
}
// Helper function to log state transitions with stack state
static void logStateTransition(DeviceFrameworkTemplateContext& ctx, const String& fromState, const String& toState, const String& reason = "") {
if (!deviceFrameworkTemplateEngineLogger) {
return;
}
String msg = "State: " + fromState + " -> " + toState;
if (reason.length() > 0) {
msg += " (" + reason + ")";
}
msg += " | Stack depth: " + String(ctx.renderingDepth);
if (ctx.renderingDepth > 0) {
RenderingContext* currentCtx = ctx.getCurrentContext();
msg += " | Current: " + String(currentCtx->name) + " (type=" + getContextTypeString(currentCtx->type) + ")";
}
deviceFrameworkTemplateEngineLogger->debug(msg);
}
static String getStateName(TemplateRenderState state) {
switch (state) {
case TemplateRenderState::TEXT: return "TEXT";
case TemplateRenderState::BUILDING_PLACEHOLDER: return "BUILDING_PLACEHOLDER";
case TemplateRenderState::RENDERING_CONTEXT: return "RENDERING_CONTEXT";
case TemplateRenderState::COMPLETE: return "COMPLETE";
case TemplateRenderState::ERROR: return "ERROR";
default: return "UNKNOWN";
}
}
DeviceFrameworkTemplateRenderer::RenderOutcome DeviceFrameworkTemplateRenderer::makeWritten(size_t bytes, TemplateRenderState state, bool repeat) {
return {bytes, state, repeat, false, false, 0, {false, RenderingContextType::TEMPLATE, nullptr}};
}
DeviceFrameworkTemplateRenderer::RenderOutcome DeviceFrameworkTemplateRenderer::makeState(TemplateRenderState nextState, bool repeat) {
return {0, nextState, repeat, false, false, 0, {false, RenderingContextType::TEMPLATE, nullptr}};
}
DeviceFrameworkTemplateRenderer::RenderOutcome DeviceFrameworkTemplateRenderer::makeComplete() {
return {0, TemplateRenderState::COMPLETE, false, true, false, 0, {false, RenderingContextType::TEMPLATE, nullptr}};
}
DeviceFrameworkTemplateRenderer::RenderOutcome DeviceFrameworkTemplateRenderer::makeError() {
return {0, TemplateRenderState::ERROR, false, false, true, 0, {false, RenderingContextType::TEMPLATE, nullptr}};
}
DeviceFrameworkTemplateRenderer::RenderOutcome DeviceFrameworkTemplateRenderer::renderChunk(DeviceFrameworkTemplateContext& ctx, uint8_t* buffer, size_t maxLen) {
RenderOutcome outcome = makeState(ctx.state, false);
switch (ctx.state) {
case TemplateRenderState::TEXT:
outcome = consumeTemplateText(ctx, buffer, maxLen);
break;
case TemplateRenderState::BUILDING_PLACEHOLDER:
outcome = buildPlaceholderToken(ctx);
break;
case TemplateRenderState::RENDERING_CONTEXT:
outcome = emitActiveContext(ctx, buffer, maxLen);
break;
case TemplateRenderState::COMPLETE:
return makeComplete();
case TemplateRenderState::ERROR:
default:
return makeError();
}
if (!applyStackCommands(ctx, outcome)) {
ctx.state = TemplateRenderState::ERROR;
return makeError();
}
TemplateRenderState previousState = ctx.state;
if (outcome.nextState != previousState) {
String fromState = getStateName(previousState);
ctx.state = outcome.nextState;
String toState = getStateName(ctx.state);
logStateTransition(ctx, fromState, toState);
} else {
ctx.state = outcome.nextState;
}
outcome.finished = (ctx.state == TemplateRenderState::COMPLETE);
outcome.errored = (ctx.state == TemplateRenderState::ERROR);
return outcome;
}
bool DeviceFrameworkTemplateRenderer::applyStackCommands(DeviceFrameworkTemplateContext& ctx, const RenderOutcome& outcome) {
for (uint8_t i = 0; i < outcome.popCount; ++i) {
if (ctx.renderingDepth == 0) {
DFTE_LOG_ERROR("Rendering stack underflow during pop");
return false;
}
ctx.popContext();
}
if (!outcome.pushContext.active) {
return true;
}
const PlaceholderEntry* entry = outcome.pushContext.entry;
if (entry == nullptr) {
DFTE_LOG_ERROR("Missing placeholder entry for pushContext");
return false;
}
const char* name = entry->name;
switch (outcome.pushContext.type) {
case RenderingContextType::PLACEHOLDER_DATA: {
if (!pushPlaceholderEntry(ctx, entry, name)) {
return false;
}
return true;
}
case RenderingContextType::PLACEHOLDER_TEMPLATE: {
if (!pushPlaceholderEntry(ctx, entry, name)) {
return false;
}
return true;
}
case RenderingContextType::PLACEHOLDER_DYNAMIC_TEMPLATE: {
if (!pushPlaceholderEntry(ctx, entry, name)) {
return false;
}
return true;
}
case RenderingContextType::PLACEHOLDER_CONDITIONAL: {
const ConditionalDescriptor* descriptor = static_cast<const ConditionalDescriptor*>(entry->data);
if (descriptor == nullptr || descriptor->evaluate == nullptr) {
DFTE_LOG_ERROR("Conditional placeholder missing descriptor: " + String(name));
return false;
}
ConditionalBranchResult branch = descriptor->evaluate(descriptor->userData);
const char* delegateName = nullptr;
switch (branch) {
case ConditionalBranchResult::TRUE_BRANCH:
delegateName = descriptor->truePlaceholder;
break;
case ConditionalBranchResult::FALSE_BRANCH:
delegateName = descriptor->falsePlaceholder;
break;
case ConditionalBranchResult::SKIP:
default:
delegateName = nullptr;
break;
}
if (delegateName == nullptr) {
return true; // Nothing to render
}
const PlaceholderEntry* delegateEntry = ctx.registry ? ctx.registry->getPlaceholder(delegateName) : nullptr;
if (!delegateEntry) {
DFTE_LOG_WARN("Conditional placeholder '" + String(name) + "' referenced unknown placeholder: " + String(delegateName));
return true;
}
if (!ctx.pushContext(RenderingContextType::PLACEHOLDER_CONDITIONAL, name)) {
return false;
}
RenderingContext* conditionalCtx = ctx.getCurrentContext();
conditionalCtx->context.conditional.entry = entry;
conditionalCtx->context.conditional.descriptor = descriptor;
conditionalCtx->context.conditional.branchResolved = true;
conditionalCtx->context.conditional.delegateName = delegateName;
conditionalCtx->context.conditional.delegateEntry = delegateEntry;
if (!pushPlaceholderEntry(ctx, delegateEntry, delegateName)) {
ctx.popContext();
return false;
}
return true;
}
case RenderingContextType::PLACEHOLDER_ITERATOR: {
if (!ctx.pushContext(RenderingContextType::PLACEHOLDER_ITERATOR, name)) {
return false;
}
RenderingContext* iteratorCtx = ctx.getCurrentContext();
iteratorCtx->context.iterator.entry = entry;
iteratorCtx->context.iterator.descriptor = static_cast<const IteratorDescriptor*>(entry->data);
iteratorCtx->context.iterator.handle = nullptr;
iteratorCtx->context.iterator.initialized = false;
iteratorCtx->context.iterator.handleOpen = false;
if (!iteratorCtx->context.iterator.descriptor || !iteratorCtx->context.iterator.descriptor->next) {
DFTE_LOG_ERROR("Iterator placeholder missing next handler: " + String(name));
ctx.popContext();
return false;
}
return true;
}
case RenderingContextType::TEMPLATE: {
if (!ctx.pushContext(RenderingContextType::TEMPLATE, name)) {
return false;
}
RenderingContext* templateCtx = ctx.getCurrentContext();
templateCtx->context.templateCtx.templateData = static_cast<const char*>(entry->data);
templateCtx->context.templateCtx.templateLen = entry->getLength(entry->data);
templateCtx->context.templateCtx.isProgmem = true;
templateCtx->context.templateCtx.position = 0;
templateCtx->context.templateCtx.iteratorPlaceholders = nullptr;
templateCtx->context.templateCtx.iteratorPlaceholderCount = 0;
return true;
}
default:
return true;
}
}
DeviceFrameworkTemplateRenderer::RenderOutcome DeviceFrameworkTemplateRenderer::handleTemplateCompletion(DeviceFrameworkTemplateContext& ctx) {
RenderOutcome outcome = makeState(TemplateRenderState::TEXT, true);
outcome.popCount = 1;
if (ctx.renderingDepth <= 1) {
outcome.nextState = TemplateRenderState::COMPLETE;
outcome.repeat = false;
outcome.finished = true;
return outcome;
}
RenderingContext* parentCtx = ctx.getContext(ctx.renderingDepth - 2);
if (parentCtx && (parentCtx->type == RenderingContextType::PLACEHOLDER_TEMPLATE ||
parentCtx->type == RenderingContextType::PLACEHOLDER_DYNAMIC_TEMPLATE ||
parentCtx->type == RenderingContextType::PLACEHOLDER_CONDITIONAL)) {
outcome.popCount += 1;
if (ctx.renderingDepth > 2) {
parentCtx = ctx.getContext(ctx.renderingDepth - 3);
} else {
parentCtx = nullptr;
}
}
if (!parentCtx) {
outcome.nextState = TemplateRenderState::COMPLETE;
outcome.repeat = false;
outcome.finished = true;
return outcome;
}
if (parentCtx->type == RenderingContextType::TEMPLATE) {
outcome.nextState = TemplateRenderState::TEXT;
} else {
outcome.nextState = TemplateRenderState::RENDERING_CONTEXT;
}
outcome.repeat = true;
return outcome;
}
DeviceFrameworkTemplateRenderer::RenderOutcome DeviceFrameworkTemplateRenderer::consumeTemplateText(DeviceFrameworkTemplateContext& ctx, uint8_t* buffer, size_t maxLen) {
RenderingContext* currentCtx = ctx.getCurrentContext();
if (!currentCtx || currentCtx->type != RenderingContextType::TEMPLATE) {
DFTE_LOG_ERROR("consumeTemplateText called without TEMPLATE context");
return makeError();
}
auto& templateCtx = currentCtx->context.templateCtx;
// Empty template completes immediately
if (templateCtx.templateLen == 0) {
return handleTemplateCompletion(ctx);
}
size_t written = 0;
while (written < maxLen) {
if (templateCtx.position >= templateCtx.templateLen) {
break;
}
char c = ctx.getNextChar();
if (c == '\0') {
break;
}
if (c == '%') {
ctx.placeholderPos = 0;
ctx.placeholderName[ctx.placeholderPos++] = '%';
RenderOutcome outcome = makeState(TemplateRenderState::BUILDING_PLACEHOLDER, true);
outcome.bytesWritten = written;
return outcome;
}
buffer[written++] = c;
}
if (written > 0) {
RenderOutcome outcome = makeWritten(written, TemplateRenderState::TEXT, written < maxLen);
if (written < maxLen) {
outcome.repeat = true;
}
return outcome;
}
// No bytes written, check if template finished
if (templateCtx.position >= templateCtx.templateLen || !ctx.hasMoreData()) {
return handleTemplateCompletion(ctx);
}
// Need more data
return makeState(TemplateRenderState::TEXT, false);
}
DeviceFrameworkTemplateRenderer::RenderOutcome DeviceFrameworkTemplateRenderer::buildPlaceholderToken(DeviceFrameworkTemplateContext& ctx) {
RenderingContext* currentCtx = ctx.getCurrentContext();
if (!currentCtx || currentCtx->type != RenderingContextType::TEMPLATE) {
DFTE_LOG_ERROR("buildPlaceholderToken called without TEMPLATE context");
return makeError();
}
bool madeProgress = false;
while (ctx.placeholderPos < sizeof(ctx.placeholderName) - 1) {
if (!ctx.hasMoreData()) {
break;
}
char c = ctx.getNextChar();
if (c == '\0') {
break;
}
madeProgress = true;
ctx.placeholderName[ctx.placeholderPos++] = c;
if (c == '%') {
ctx.placeholderName[ctx.placeholderPos] = '\0';
return resolvePlaceholder(ctx);
}
}
if (ctx.placeholderPos >= sizeof(ctx.placeholderName) - 1) {
DFTE_LOG_WARN("Placeholder name too long: " + String(ctx.placeholderName));
ctx.resetPlaceholder();
return makeState(TemplateRenderState::TEXT, true);
}
if (!ctx.hasMoreData()) {
DFTE_LOG_WARN("Incomplete placeholder at end of template");
ctx.resetPlaceholder();
return handleTemplateCompletion(ctx);
}
// We consumed data but did not finish; stay in BUILDING_PLACEHOLDER
RenderOutcome outcome = makeState(TemplateRenderState::BUILDING_PLACEHOLDER, false);
outcome.repeat = madeProgress;
return outcome;
}
DeviceFrameworkTemplateRenderer::RenderOutcome DeviceFrameworkTemplateRenderer::resolvePlaceholder(DeviceFrameworkTemplateContext& ctx) {
const PlaceholderEntry* entry = ctx.registry ? ctx.registry->getPlaceholder(ctx.placeholderName) : nullptr;
if (!entry) {
RenderingContext* currentCtx = ctx.getCurrentContext();
if (currentCtx && currentCtx->type == RenderingContextType::TEMPLATE) {
const PlaceholderEntry* overrides = currentCtx->context.templateCtx.iteratorPlaceholders;
size_t overrideCount = currentCtx->context.templateCtx.iteratorPlaceholderCount;
for (size_t i = 0; i < overrideCount; ++i) {
if (strcmp(overrides[i].name, ctx.placeholderName) == 0) {
entry = &overrides[i];
break;
}
}
}
}
if (!entry) {
DFTE_LOG_WARN("Unknown placeholder: " + String(ctx.placeholderName));
ctx.resetPlaceholder();
return makeState(TemplateRenderState::TEXT, true);
}
RenderOutcome outcome = makeState(TemplateRenderState::RENDERING_CONTEXT, true);
outcome.pushContext.active = true;
switch (entry->type) {
case PlaceholderType::PROGMEM_DATA:
case PlaceholderType::RAM_DATA:
outcome.pushContext.type = RenderingContextType::PLACEHOLDER_DATA;
outcome.pushContext.entry = entry;
break;
case PlaceholderType::PROGMEM_TEMPLATE:
outcome.pushContext.type = RenderingContextType::PLACEHOLDER_TEMPLATE;
outcome.pushContext.entry = entry;
outcome.nextState = TemplateRenderState::TEXT;
break;
case PlaceholderType::DYNAMIC_TEMPLATE:
outcome.pushContext.type = RenderingContextType::PLACEHOLDER_DYNAMIC_TEMPLATE;
outcome.pushContext.entry = entry;
outcome.nextState = TemplateRenderState::TEXT;
break;
case PlaceholderType::CONDITIONAL:
outcome.pushContext.type = RenderingContextType::PLACEHOLDER_CONDITIONAL;
outcome.pushContext.entry = entry;
outcome.nextState = TemplateRenderState::RENDERING_CONTEXT;
break;
case PlaceholderType::ITERATOR:
outcome.pushContext.type = RenderingContextType::PLACEHOLDER_ITERATOR;
outcome.pushContext.entry = entry;
outcome.nextState = TemplateRenderState::RENDERING_CONTEXT;
break;
default:
DFTE_LOG_WARN("Unsupported placeholder type");
ctx.resetPlaceholder();
return makeState(TemplateRenderState::TEXT, true);
}
ctx.resetPlaceholder();
return outcome;
}
DeviceFrameworkTemplateRenderer::RenderOutcome DeviceFrameworkTemplateRenderer::emitActiveContext(DeviceFrameworkTemplateContext& ctx, uint8_t* buffer, size_t maxLen) {
RenderingContext* currentCtx = ctx.getCurrentContext();
if (!currentCtx) {
return makeComplete();
}
switch (currentCtx->type) {
case RenderingContextType::TEMPLATE:
return consumeTemplateText(ctx, buffer, maxLen);
case RenderingContextType::PLACEHOLDER_DATA:
return streamPlaceholderData(ctx, currentCtx, buffer, maxLen);
case RenderingContextType::PLACEHOLDER_TEMPLATE:
return makeState(TemplateRenderState::TEXT, true);
case RenderingContextType::PLACEHOLDER_DYNAMIC_TEMPLATE:
return makeState(TemplateRenderState::TEXT, true);
case RenderingContextType::PLACEHOLDER_CONDITIONAL: {
RenderOutcome outcome = makeState(TemplateRenderState::RENDERING_CONTEXT, true);
outcome.popCount = 1;
RenderingContext* parent = (ctx.renderingDepth > 1) ? ctx.getContext(ctx.renderingDepth - 2) : nullptr;
if (!parent) {
outcome.nextState = TemplateRenderState::COMPLETE;
outcome.repeat = false;
outcome.finished = true;
} else if (parent->type == RenderingContextType::TEMPLATE) {
outcome.nextState = TemplateRenderState::TEXT;
}
return outcome;
}
case RenderingContextType::PLACEHOLDER_ITERATOR:
return processIteratorContext(ctx, currentCtx);
default:
DFTE_LOG_ERROR("emitActiveContext encountered unknown context type");
return makeError();
}
}
DeviceFrameworkTemplateRenderer::RenderOutcome DeviceFrameworkTemplateRenderer::streamPlaceholderData(DeviceFrameworkTemplateContext& ctx,
RenderingContext* context,
uint8_t* buffer,
size_t maxLen) {
auto& dataCtx = context->context.data;
const PlaceholderEntry* entry = dataCtx.entry;
if (!entry) {
DFTE_LOG_ERROR("Placeholder data context missing entry");
RenderOutcome outcome = makeState(TemplateRenderState::RENDERING_CONTEXT, true);
outcome.popCount = 1;
return outcome;
}
if (ctx.registry == nullptr) {
DFTE_LOG_ERROR("Placeholder registry not set; cannot render placeholder: " + String(entry->name));
RenderOutcome outcome = makeState(TemplateRenderState::RENDERING_CONTEXT, true);
outcome.popCount = 1;
RenderingContext* parent = (ctx.renderingDepth > 1) ? ctx.getContext(ctx.renderingDepth - 2) : nullptr;
if (!parent) {
outcome.nextState = TemplateRenderState::COMPLETE;
outcome.repeat = false;
outcome.finished = true;
} else if (parent->type == RenderingContextType::TEMPLATE) {
outcome.nextState = TemplateRenderState::TEXT;
}
return outcome;
}
size_t totalLength = 0;
if (entry->hasCachedLength) {
totalLength = entry->cachedLength;
} else if (entry->getLength != nullptr) {
totalLength = entry->getLength(entry->data);
} else {
DFTE_LOG_ERROR("Placeholder '" + String(entry->name) + "' missing length getter");
return makeError();
}
if (dataCtx.offset >= totalLength) {
RenderOutcome outcome = makeState(TemplateRenderState::RENDERING_CONTEXT, true);
outcome.popCount = 1;
RenderingContext* parent = (ctx.renderingDepth > 1) ? ctx.getContext(ctx.renderingDepth - 2) : nullptr;
if (parent && parent->type == RenderingContextType::PLACEHOLDER_CONDITIONAL) {
outcome.popCount += 1;
parent = (ctx.renderingDepth > 2) ? ctx.getContext(ctx.renderingDepth - 3) : nullptr;
}
if (!parent) {
outcome.nextState = TemplateRenderState::COMPLETE;
outcome.repeat = false;
outcome.finished = true;
} else if (parent->type == RenderingContextType::TEMPLATE) {
outcome.nextState = TemplateRenderState::TEXT;
}
return outcome;
}
size_t written = ctx.registry->renderPlaceholder(entry, dataCtx.offset, buffer, maxLen);
if (written > 0) {
dataCtx.offset += written;
return makeWritten(written, TemplateRenderState::RENDERING_CONTEXT, written < maxLen);
}
// No bytes written; treat as completion and pop context
RenderOutcome outcome = makeState(TemplateRenderState::RENDERING_CONTEXT, true);
outcome.popCount = 1;
RenderingContext* parent = (ctx.renderingDepth > 1) ? ctx.getContext(ctx.renderingDepth - 2) : nullptr;
if (parent && parent->type == RenderingContextType::PLACEHOLDER_CONDITIONAL) {
outcome.popCount += 1;
parent = (ctx.renderingDepth > 2) ? ctx.getContext(ctx.renderingDepth - 3) : nullptr;
}
if (!parent) {
outcome.nextState = TemplateRenderState::COMPLETE;
outcome.repeat = false;
outcome.finished = true;
} else if (parent->type == RenderingContextType::TEMPLATE) {
outcome.nextState = TemplateRenderState::TEXT;
}
return outcome;
}
size_t DeviceFrameworkTemplateRenderer::renderNextChunk(DeviceFrameworkTemplateContext& ctx, uint8_t* buffer, size_t maxLen) {
if (ctx.isComplete() || ctx.hasError()) {
return 0;
}
size_t written = 0;
size_t iterations = 0;
size_t consecutiveNoProgressIterations = 0;
uint8_t* writePtr = buffer;
size_t remaining = maxLen;
while (remaining > 0 && !ctx.isComplete() && !ctx.hasError() && iterations < MAX_ITERATIONS) {
RenderOutcome outcome = renderChunk(ctx, writePtr, remaining);
bool noProgressIteration = (outcome.bytesWritten == 0 && outcome.repeat && !outcome.finished && !outcome.errored);
if (noProgressIteration) {
consecutiveNoProgressIterations++;
} else {
consecutiveNoProgressIterations = 0;
}
written += outcome.bytesWritten;
ctx.totalBytesProcessed += outcome.bytesWritten;
writePtr += outcome.bytesWritten;
remaining -= outcome.bytesWritten;
iterations++;
if (outcome.finished || outcome.errored) {
break;
}
if (!outcome.repeat && outcome.bytesWritten == 0) {
break;
}
}
if (iterations >= MAX_ITERATIONS) {
DFTE_LOG_WARN("Maximum iterations reached in renderNextChunk");
if (consecutiveNoProgressIterations >= 3 && !ctx.isComplete() && !ctx.hasError()) {
ctx.state = TemplateRenderState::ERROR;
}
}
return written;
}
void DeviceFrameworkTemplateRenderer::initializeContext(DeviceFrameworkTemplateContext& ctx, const char* templateData) {
initializeContext(ctx, templateData, true);
}
void DeviceFrameworkTemplateRenderer::initializeContext(DeviceFrameworkTemplateContext& ctx, const char* templateData, bool templateInProgmem) {
ctx.reset();
if (templateData == nullptr) {
DFTE_LOG_ERROR("initializeContext called with null template pointer");
ctx.state = TemplateRenderState::ERROR;
return;
}
// Push initial template context
if (!ctx.pushContext(RenderingContextType::TEMPLATE, "ROOT")) {
ctx.state = TemplateRenderState::ERROR;
return;
}
RenderingContext* rootCtx = ctx.getCurrentContext();
rootCtx->context.templateCtx.templateData = templateData;
rootCtx->context.templateCtx.templateLen = templateInProgmem ? strlen_P(templateData) : strlen(templateData);
rootCtx->context.templateCtx.isProgmem = templateInProgmem;
rootCtx->context.templateCtx.position = 0;
ctx.state = TemplateRenderState::TEXT;
logStateTransition(ctx, "INIT", "TEXT", "Initialized template context");
}
bool DeviceFrameworkTemplateRenderer::isComplete(const DeviceFrameworkTemplateContext& ctx) {
return ctx.isComplete();
}
bool DeviceFrameworkTemplateRenderer::hasError(const DeviceFrameworkTemplateContext& ctx) {
return ctx.hasError();
}