From 5719aa6c39aa73c84ea5c48977079c13cdc687b7 Mon Sep 17 00:00:00 2001 From: Alex Hope-O'Connor Date: Sun, 9 Nov 2025 10:17:57 +1000 Subject: [PATCH] Add DFTE sources and tests --- .gitignore | 24 + README.md | 27 + include/DeviceFrameworkPlaceholderRegistry.h | 144 +++ include/DeviceFrameworkTemplateContext.h | 114 ++ include/DeviceFrameworkTemplateEngineDebug.h | 121 ++ include/DeviceFrameworkTemplateRenderer.h | 111 ++ include/DeviceFrameworkTemplateTypes.h | 204 ++++ include/TemplateEngine.h | 55 + library.json | 33 + platformio.ini | 6 + src/DeviceFrameworkPlaceholderRegistry.cpp | 294 +++++ src/DeviceFrameworkTemplateContext.cpp | 260 +++++ src/DeviceFrameworkTemplateEngineDebug.cpp | 28 + src/DeviceFrameworkTemplateRenderer.cpp | 860 ++++++++++++++ .../templates/edge_case_templates.h | 66 ++ .../templates/nested_templates.h | 163 +++ .../templates/placeholder_templates.h | 38 + .../templates/simple_templates.h | 35 + test/test_template_engine/test_main.cpp | 139 +++ test/test_template_engine/test_main.h | 56 + .../tests/test_edge_cases.cpp | 307 +++++ .../tests/test_integration.cpp | 198 ++++ .../tests/test_placeholder_registry.cpp | 255 ++++ .../tests/test_template_context.cpp | 324 ++++++ .../tests/test_template_renderer.cpp | 1035 +++++++++++++++++ .../test_template_engine/utils/test_utils.cpp | 62 + test/test_template_engine/utils/test_utils.h | 34 + 27 files changed, 4993 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 include/DeviceFrameworkPlaceholderRegistry.h create mode 100644 include/DeviceFrameworkTemplateContext.h create mode 100644 include/DeviceFrameworkTemplateEngineDebug.h create mode 100644 include/DeviceFrameworkTemplateRenderer.h create mode 100644 include/DeviceFrameworkTemplateTypes.h create mode 100644 include/TemplateEngine.h create mode 100644 library.json create mode 100644 platformio.ini create mode 100644 src/DeviceFrameworkPlaceholderRegistry.cpp create mode 100644 src/DeviceFrameworkTemplateContext.cpp create mode 100644 src/DeviceFrameworkTemplateEngineDebug.cpp create mode 100644 src/DeviceFrameworkTemplateRenderer.cpp create mode 100644 test/test_template_engine/templates/edge_case_templates.h create mode 100644 test/test_template_engine/templates/nested_templates.h create mode 100644 test/test_template_engine/templates/placeholder_templates.h create mode 100644 test/test_template_engine/templates/simple_templates.h create mode 100644 test/test_template_engine/test_main.cpp create mode 100644 test/test_template_engine/test_main.h create mode 100644 test/test_template_engine/tests/test_edge_cases.cpp create mode 100644 test/test_template_engine/tests/test_integration.cpp create mode 100644 test/test_template_engine/tests/test_placeholder_registry.cpp create mode 100644 test/test_template_engine/tests/test_template_context.cpp create mode 100644 test/test_template_engine/tests/test_template_renderer.cpp create mode 100644 test/test_template_engine/utils/test_utils.cpp create mode 100644 test/test_template_engine/utils/test_utils.h diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..dd0f1b1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,24 @@ +# PlatformIO build artifacts +.pio/ +.ccls-cache/ +.clangd/ +.cmake/ +cmake-build*/ +.pnpm-store/ + +# VS Code / CLion configuration +.vscode/ +.idea/ + +# macOS / Windows +.DS_Store +Thumbs.db + +# Coverage / profiling +*.gcda +*.gcno +*.gcov + +# Temporary files +*.swp +*.swo diff --git a/README.md b/README.md new file mode 100644 index 0000000..2c6820b --- /dev/null +++ b/README.md @@ -0,0 +1,27 @@ +# Device Framework Template Engine (DFTE) + +Device Framework Template Engine provides a lightweight templating layer for Arduino-based projects within the Device Framework ecosystem. It exposes a registry for dynamic placeholders, a rendering engine, and utilities for composing nested templates. + +## Structure + +- `include/` and `src/` contain the core template engine components. +- `test/test_template_engine/` hosts PlatformIO-based unit tests covering placeholder registration, context handling, renderer behaviours, and integration scenarios. + +## Getting Started + +The library is distributed as a PlatformIO library. To use it in another PlatformIO project, add the repository as a dependency in your `platformio.ini`: + +``` +lib_deps = + alexhopeoconnor/DFTE +``` + +To develop locally or contribute, clone the repository and run the tests through PlatformIO: + +``` +pio test -e test_template_engine +``` + +## License + +This project is released under the MIT License. See `LICENSE` for details. diff --git a/include/DeviceFrameworkPlaceholderRegistry.h b/include/DeviceFrameworkPlaceholderRegistry.h new file mode 100644 index 0000000..17a8e3b --- /dev/null +++ b/include/DeviceFrameworkPlaceholderRegistry.h @@ -0,0 +1,144 @@ +#ifndef DEVICEFRAMEWORK_PLACEHOLDER_REGISTRY_H +#define DEVICEFRAMEWORK_PLACEHOLDER_REGISTRY_H + +#include +#include +#include "DeviceFrameworkTemplateTypes.h" + +// Fallback defaults when DeviceFrameworkConfig is not available (standalone usage) +// Always use internal macro names (DFTE_*) to avoid conflicts with DeviceFrameworkConfig extern declarations +// When DeviceFrameworkConfig is included, extern variables are declared with CONFIG_* names +// We use DFTE_* internal names for compile-time constants (constexpr) and default parameter values +// NEVER define CONFIG_* macros here to avoid conflicts with extern declarations in DeviceFrameworkConfig +#ifndef DFTE_PLACEHOLDER_NAME_SIZE_DEFAULT + #define DFTE_PLACEHOLDER_NAME_SIZE_DEFAULT 24 +#endif + +#ifndef DFTE_MAX_PLACEHOLDERS_DEFAULT + #define DFTE_MAX_PLACEHOLDERS_DEFAULT 16 +#endif + +#ifndef DFTE_PROGMEM_CHUNK_SIZE_DEFAULT + #define DFTE_PROGMEM_CHUNK_SIZE_DEFAULT 512 +#endif + +#ifndef DFTE_RAM_CHUNK_SIZE_DEFAULT + #define DFTE_RAM_CHUNK_SIZE_DEFAULT 128 +#endif + +// Use DeviceFramework config defaults at compile-time if available, otherwise use internal defaults +#ifdef DEVICEFRAMEWORK_CONFIG_H + // DeviceFramework is present - use config defaults + // DeviceFrameworkConfig.h must be included before this file to access CONFIG_*_default macros + #ifdef CONFIG_templateProgmemChunkSize_default + #define DFTE_PROGMEM_CHUNK_SIZE CONFIG_templateProgmemChunkSize_default + #else + #define DFTE_PROGMEM_CHUNK_SIZE DFTE_PROGMEM_CHUNK_SIZE_DEFAULT + #endif + #ifdef CONFIG_templateRamChunkSize_default + #define DFTE_RAM_CHUNK_SIZE CONFIG_templateRamChunkSize_default + #else + #define DFTE_RAM_CHUNK_SIZE DFTE_RAM_CHUNK_SIZE_DEFAULT + #endif +#else + // Standalone usage - use internal defaults + #define DFTE_PROGMEM_CHUNK_SIZE DFTE_PROGMEM_CHUNK_SIZE_DEFAULT + #define DFTE_RAM_CHUNK_SIZE DFTE_RAM_CHUNK_SIZE_DEFAULT +#endif + +/** + * DeviceFramework Placeholder Registry + * Manages runtime registration of template placeholders + * Registry starts empty - users must register their placeholders + */ +class DeviceFrameworkPlaceholderRegistry { +public: + /** + * Constructor with optional max placeholders parameter + * @param maxPlaceholders Maximum number of placeholders (defaults to DFTE_MAX_PLACEHOLDERS_DEFAULT) + * Allows creating multiple registries with different sizes + * When using with DeviceFramework, pass getConfigMaxTemplatePlaceholders() for runtime config + */ + explicit DeviceFrameworkPlaceholderRegistry(uint16_t maxPlaceholders = DFTE_MAX_PLACEHOLDERS_DEFAULT); + + ~DeviceFrameworkPlaceholderRegistry(); + + // Prevent copying + DeviceFrameworkPlaceholderRegistry(const DeviceFrameworkPlaceholderRegistry&) = delete; + DeviceFrameworkPlaceholderRegistry& operator=(const DeviceFrameworkPlaceholderRegistry&) = delete; + + /** + * Register a PROGMEM data placeholder + * @param name Placeholder name (e.g., "%CSS%") + * @param progmemData Pointer to PROGMEM data + * @return true if registered successfully, false if registry full + */ + bool registerProgmemData(const char* name, const char* progmemData); + + /** + * Register a PROGMEM template placeholder (nested template) + * @param name Placeholder name (e.g., "%HEADER%") + * @param progmemTemplate Pointer to PROGMEM template + * @return true if registered successfully + */ + bool registerProgmemTemplate(const char* name, const char* progmemTemplate); + + /** + * Register a RAM data placeholder with getter function + * @param name Placeholder name (e.g., "%PAGE_TITLE%") + * @param getter Function that returns current value + * @return true if registered successfully + */ + bool registerRamData(const char* name, PlaceholderDataGetter getter); + bool registerDynamicTemplate(const char* name, const DynamicTemplateDescriptor* descriptor); + bool registerConditional(const char* name, const ConditionalDescriptor* descriptor); + bool registerIterator(const char* name, const IteratorDescriptor* descriptor); + + /** + * Clear all registered placeholders + */ + void clear(); + + /** + * Get number of registered placeholders + */ + int getCount() const { return count; } + + /** + * Get maximum number of placeholders + */ + uint16_t getMaxPlaceholders() const { return maxPlaceholders; } + + /** + * Find placeholder entry by name + * @return PlaceholderEntry pointer or nullptr if not found + */ + const PlaceholderEntry* getPlaceholder(const char* name) const; + + /** + * Render placeholder content at given offset + */ + size_t renderPlaceholder(const PlaceholderEntry* entry, size_t offset, + uint8_t* buffer, size_t maxLen) const; + + // Static helper functions for length calculation + static size_t getProgmemLength(const void* data); + static size_t getRamLength(const void* data); + static size_t getDynamicTemplateLength(const DynamicTemplateDescriptor* descriptor, const char* templateData); + +private: + static constexpr uint16_t MAX_PLACEHOLDER_NAME_SIZE = DFTE_PLACEHOLDER_NAME_SIZE_DEFAULT; + + PlaceholderEntry* placeholders; // Dynamically allocated array + uint16_t maxPlaceholders; // Configurable size + int count; + + bool validatePlaceholderName(const char* name) const; + static size_t copyProgmemData(const char* source, size_t offset, + uint8_t* dest, size_t maxLen); + static size_t copyRamData(PlaceholderDataGetter getter, size_t offset, + uint8_t* dest, size_t maxLen); +}; + +#endif // DEVICEFRAMEWORK_PLACEHOLDER_REGISTRY_H + diff --git a/include/DeviceFrameworkTemplateContext.h b/include/DeviceFrameworkTemplateContext.h new file mode 100644 index 0000000..e5fb0c4 --- /dev/null +++ b/include/DeviceFrameworkTemplateContext.h @@ -0,0 +1,114 @@ +#ifndef DEVICEFRAMEWORK_TEMPLATE_CONTEXT_H +#define DEVICEFRAMEWORK_TEMPLATE_CONTEXT_H + +#include +#include "DeviceFrameworkTemplateTypes.h" + +// Fallback defaults when DeviceFrameworkConfig is not available (standalone usage) +// Always use internal macro names (DFTE_*) to avoid conflicts with DeviceFrameworkConfig extern declarations +// When DeviceFrameworkConfig is included, extern variables are declared with CONFIG_* names +// We use DFTE_* internal names for compile-time constants (constexpr) and array sizes +#ifndef DFTE_MAX_STACK_DEPTH_DEFAULT + #define DFTE_MAX_STACK_DEPTH_DEFAULT 16 +#endif + +#ifndef DFTE_BUFFER_SIZE_DEFAULT + #define DFTE_BUFFER_SIZE_DEFAULT 512 +#endif + +#ifndef DFTE_PLACEHOLDER_NAME_SIZE_DEFAULT + #define DFTE_PLACEHOLDER_NAME_SIZE_DEFAULT 24 +#endif + +// Use DeviceFramework config defaults at compile-time if available, otherwise use internal defaults +// Array sizes must be compile-time constants +#ifdef DEVICEFRAMEWORK_CONFIG_H + // DeviceFramework is present - use config defaults for array sizing + // DeviceFrameworkConfig.h must be included before this file to access CONFIG_*_default macros + #ifdef CONFIG_templateStackDepth_default + #define DFTE_MAX_STACK_DEPTH CONFIG_templateStackDepth_default + #else + #define DFTE_MAX_STACK_DEPTH DFTE_MAX_STACK_DEPTH_DEFAULT + #endif + #ifdef CONFIG_templateBufferSize_default + #define DFTE_BUFFER_SIZE CONFIG_templateBufferSize_default + #else + #define DFTE_BUFFER_SIZE DFTE_BUFFER_SIZE_DEFAULT + #endif + // DFTE_PLACEHOLDER_NAME_SIZE is already defined in DeviceFrameworkTemplateTypes.h +#else + // Standalone usage - use internal defaults + #define DFTE_MAX_STACK_DEPTH DFTE_MAX_STACK_DEPTH_DEFAULT + #define DFTE_BUFFER_SIZE DFTE_BUFFER_SIZE_DEFAULT + // DFTE_PLACEHOLDER_NAME_SIZE is already defined in DeviceFrameworkTemplateTypes.h +#endif + +// Forward declaration +class DeviceFrameworkPlaceholderRegistry; + +/** + * Template rendering context + * Maintains state for chunked streaming rendering with nested template support + */ +class DeviceFrameworkTemplateContext { +public: + using State = TemplateRenderState; + + // Context state + State state; + + // Unified rendering stack + static constexpr int MAX_RENDERING_DEPTH = DFTE_MAX_STACK_DEPTH; + RenderingContext renderingStack[MAX_RENDERING_DEPTH]; + int renderingDepth; + + // Current placeholder being built (only valid during BUILDING_PLACEHOLDER state) + // Allocated to configured size - matches CONFIG_templatePlaceholderNameSize when DeviceFramework is present + char placeholderName[DFTE_PLACEHOLDER_NAME_SIZE]; + size_t placeholderPos; + + // Centralized buffer management + // Allocated to configured size - matches CONFIG_templateBufferSize when DeviceFramework is present + static const size_t BUFFER_SIZE = DFTE_BUFFER_SIZE; + uint8_t readBuffer[BUFFER_SIZE]; + size_t bufferPos; + size_t bufferLen; + size_t bufferOffset; + + // Placeholder registry (injected, not owned) + DeviceFrameworkPlaceholderRegistry* registry; + + // Statistics + size_t totalBytesProcessed; + unsigned long startTime; + + DeviceFrameworkTemplateContext(); + void reset(); + + // Unified stack management methods + bool pushContext(RenderingContextType type, const char* name); + void popContext(); + RenderingContext* getCurrentContext(); + RenderingContext* getContext(int depth); + bool isRenderingTemplate() const; + bool isRenderingPlaceholder() const; + RenderingContextType getCurrentContextType() const; + + bool isComplete() const; + bool hasError() const; + String getStateString() const; + String getStackTrace() const; + + // Set the registry to use for placeholder lookups + void setRegistry(DeviceFrameworkPlaceholderRegistry* reg) { registry = reg; } + + // Unified buffer management + bool refillBuffer(); + char getNextChar(); + size_t getAvailableBytes() const; + bool hasMoreData() const; + void resetPlaceholder(); +}; + +#endif // DEVICEFRAMEWORK_TEMPLATE_CONTEXT_H + diff --git a/include/DeviceFrameworkTemplateEngineDebug.h b/include/DeviceFrameworkTemplateEngineDebug.h new file mode 100644 index 0000000..23553b9 --- /dev/null +++ b/include/DeviceFrameworkTemplateEngineDebug.h @@ -0,0 +1,121 @@ +#ifndef DEVICEFRAMEWORK_TEMPLATE_ENGINE_DEBUG_H +#define DEVICEFRAMEWORK_TEMPLATE_ENGINE_DEBUG_H + +#include + +// ============================================================================ +// LOGGING INTERFACE - Framework Agnostic +// ============================================================================ + +/** + * Logging interface for the DeviceFramework Template Engine + * + * This interface allows any logging system to be plugged into the template engine. + * The template engine itself remains completely framework-agnostic. + * + * Usage: + * 1. Implement this interface with your preferred logging system + * 2. Call deviceFrameworkTemplateEngineEnableLogging() with your logger instance + * 3. All DFTE_LOG_* macros will then use your logger + * + * Example: + * class MyLogger : public DeviceFrameworkTemplateEngineLogger { + * void error(const String& msg) override { // your logging logic } + * void warn(const String& msg) override { // your logging logic } + * void info(const String& msg) override { // your logging logic } + * void debug(const String& msg) override { // your logging logic } + * }; + * + * MyLogger myLogger; + * deviceFrameworkTemplateEngineEnableLogging(&myLogger); + */ +class DeviceFrameworkTemplateEngineLogger { +public: + virtual void error(const String& msg) = 0; + virtual void warn(const String& msg) = 0; + virtual void info(const String& msg) = 0; + virtual void debug(const String& msg) = 0; + virtual ~DeviceFrameworkTemplateEngineLogger() {} +}; + +// Global logger pointer - set by user or defaults to nullptr (disabled) +extern DeviceFrameworkTemplateEngineLogger* deviceFrameworkTemplateEngineLogger; + +// ============================================================================ +// LOGGING MACROS - NO-OPS BY DEFAULT +// ============================================================================ + +/** + * DeviceFramework Template Engine Logging System + * + * LOGGING IS DISABLED BY DEFAULT - All macros are no-ops unless explicitly enabled + * + * To enable logging: + * 1. Implement the DeviceFrameworkTemplateEngineLogger interface with your preferred logging system + * 2. Call deviceFrameworkTemplateEngineEnableLogging() with your logger instance + * + * This design ensures: + * - Zero logging overhead by default + * - Complete framework decoupling + * - Flexible logging system integration + * - No build flags or conditional compilation needed + */ + +// All logging macros are no-ops by default - zero overhead unless explicitly enabled +#define DFTE_LOG_ERROR(msg) do { \ + if (deviceFrameworkTemplateEngineLogger) { \ + deviceFrameworkTemplateEngineLogger->error(msg); \ + } \ +} while(0) + +#define DFTE_LOG_WARN(msg) do { \ + if (deviceFrameworkTemplateEngineLogger) { \ + deviceFrameworkTemplateEngineLogger->warn(msg); \ + } \ +} while(0) + +#define DFTE_LOG_INFO(msg) do { \ + if (deviceFrameworkTemplateEngineLogger) { \ + deviceFrameworkTemplateEngineLogger->info(msg); \ + } \ +} while(0) + +#define DFTE_LOG_DEBUG(msg) do { \ + if (deviceFrameworkTemplateEngineLogger) { \ + deviceFrameworkTemplateEngineLogger->debug(msg); \ + } \ +} while(0) + +// ============================================================================ +// LOGGING CONFIGURATION FUNCTIONS +// ============================================================================ + +/** + * Enable logging with a custom logger implementation + * + * @param logger Pointer to custom logger implementation + * @return true if logging was enabled, false if logger is null + */ +bool deviceFrameworkTemplateEngineEnableLogging(DeviceFrameworkTemplateEngineLogger* logger); + +/** + * Disable all logging + * Sets logger to nullptr, making all DFTE_LOG_* macros no-ops + */ +void deviceFrameworkTemplateEngineDisableLogging(); + +/** + * Check if logging is currently enabled + * + * @return true if logging is enabled, false if disabled + */ +bool deviceFrameworkTemplateEngineIsLoggingEnabled(); + +/** + * Get current logger + * + * @return Pointer to current logger or nullptr if disabled + */ +DeviceFrameworkTemplateEngineLogger* deviceFrameworkTemplateEngineGetLogger(); + +#endif // DEVICEFRAMEWORK_TEMPLATE_ENGINE_DEBUG_H \ No newline at end of file diff --git a/include/DeviceFrameworkTemplateRenderer.h b/include/DeviceFrameworkTemplateRenderer.h new file mode 100644 index 0000000..6ee8b1b --- /dev/null +++ b/include/DeviceFrameworkTemplateRenderer.h @@ -0,0 +1,111 @@ +#ifndef DEVICEFRAMEWORK_TEMPLATE_RENDERER_H +#define DEVICEFRAMEWORK_TEMPLATE_RENDERER_H + +#include +#include "DeviceFrameworkTemplateContext.h" +#include "DeviceFrameworkPlaceholderRegistry.h" + +// Fallback defaults when DeviceFrameworkConfig is not available (standalone usage) +#ifndef DFTE_MAX_ITERATIONS_DEFAULT + #define DFTE_MAX_ITERATIONS_DEFAULT 50 +#endif + +// Use DeviceFramework config defaults at compile-time if available, otherwise use internal defaults +#ifdef DEVICEFRAMEWORK_CONFIG_H + // DeviceFramework is present - use config defaults + // DeviceFrameworkConfig.h must be included before this file to access CONFIG_*_default macros + #ifdef CONFIG_templateMaxIterations_default + #define DFTE_MAX_ITERATIONS CONFIG_templateMaxIterations_default + #else + #define DFTE_MAX_ITERATIONS DFTE_MAX_ITERATIONS_DEFAULT + #endif +#else + // Standalone usage - use internal defaults + #define DFTE_MAX_ITERATIONS DFTE_MAX_ITERATIONS_DEFAULT +#endif + +/** + * DeviceFramework Template Renderer + * Core rendering engine for streaming template output with chunked processing + */ +class DeviceFrameworkTemplateRenderer { +public: + struct RenderOutcome { + size_t bytesWritten; + TemplateRenderState nextState; + bool repeat; + bool finished; + bool errored; + uint8_t popCount; + struct { + bool active; + RenderingContextType type; + const PlaceholderEntry* entry; + } pushContext; + }; + + /** + * Render next chunk of template + * Call repeatedly until returns 0 + * + * @param ctx Rendering context (maintains state between calls) + * @param buffer Output buffer to write to + * @param maxLen Maximum bytes to write to buffer + * @return Number of bytes written (0 = complete or error) + */ + static size_t renderNextChunk(DeviceFrameworkTemplateContext& ctx, uint8_t* buffer, size_t maxLen); + + /** + * Initialize rendering context with template stored in PROGMEM + * Call once before rendering begins + * + * @param ctx Context to initialize + * @param templateData Pointer to PROGMEM template string + */ + static void initializeContext(DeviceFrameworkTemplateContext& ctx, const char* templateData); + + /** + * Initialize rendering context with template stored in RAM or PROGMEM + * + * @param ctx Context to initialize + * @param templateData Pointer to template string + * @param templateInProgmem True if template is stored in PROGMEM, false if in RAM + */ + static void initializeContext(DeviceFrameworkTemplateContext& ctx, const char* templateData, bool templateInProgmem); + + /** + * Check if rendering is complete + */ + static bool isComplete(const DeviceFrameworkTemplateContext& ctx); + + /** + * Check if rendering encountered an error + */ + static bool hasError(const DeviceFrameworkTemplateContext& ctx); + + /** + * Helper constructors for RenderOutcome + */ + static RenderOutcome makeWritten(size_t bytes, TemplateRenderState state, bool repeat = false); + static RenderOutcome makeState(TemplateRenderState nextState, bool repeat = true); + static RenderOutcome makeComplete(); + static RenderOutcome makeError(); + +private: + static RenderOutcome renderChunk(DeviceFrameworkTemplateContext& ctx, uint8_t* buffer, size_t maxLen); + static bool applyStackCommands(DeviceFrameworkTemplateContext& ctx, const RenderOutcome& outcome); + + // Core processing methods + static RenderOutcome consumeTemplateText(DeviceFrameworkTemplateContext& ctx, uint8_t* buffer, size_t maxLen); + static RenderOutcome buildPlaceholderToken(DeviceFrameworkTemplateContext& ctx); + static RenderOutcome resolvePlaceholder(DeviceFrameworkTemplateContext& ctx); + static RenderOutcome emitActiveContext(DeviceFrameworkTemplateContext& ctx, uint8_t* buffer, size_t maxLen); + static RenderOutcome streamPlaceholderData(DeviceFrameworkTemplateContext& ctx, RenderingContext* context, uint8_t* buffer, size_t maxLen); + static RenderOutcome handleTemplateCompletion(DeviceFrameworkTemplateContext& ctx); + + // Constants + static constexpr size_t MAX_ITERATIONS = DFTE_MAX_ITERATIONS; +}; + +#endif // DEVICEFRAMEWORK_TEMPLATE_RENDERER_H + diff --git a/include/DeviceFrameworkTemplateTypes.h b/include/DeviceFrameworkTemplateTypes.h new file mode 100644 index 0000000..0ae49cc --- /dev/null +++ b/include/DeviceFrameworkTemplateTypes.h @@ -0,0 +1,204 @@ +#ifndef DEVICEFRAMEWORK_TEMPLATE_TYPES_H +#define DEVICEFRAMEWORK_TEMPLATE_TYPES_H + +#include + +// Fallback defaults when DeviceFrameworkConfig is not available (standalone usage) +// Always use internal macro names (DFTE_*) to avoid conflicts with DeviceFrameworkConfig extern declarations +#ifndef DFTE_PLACEHOLDER_NAME_SIZE_DEFAULT + #define DFTE_PLACEHOLDER_NAME_SIZE_DEFAULT 24 +#endif + +// Use DeviceFramework config defaults at compile-time if available, otherwise use internal defaults +// Array sizes must be compile-time constants +#ifdef DEVICEFRAMEWORK_CONFIG_H + // DeviceFramework is present - use config defaults for array sizing + // DeviceFrameworkConfig.h must be included before this file to access CONFIG_*_default macros + #ifdef CONFIG_templatePlaceholderNameSize_default + #define DFTE_PLACEHOLDER_NAME_SIZE CONFIG_templatePlaceholderNameSize_default + #else + #define DFTE_PLACEHOLDER_NAME_SIZE DFTE_PLACEHOLDER_NAME_SIZE_DEFAULT + #endif +#else + // Standalone usage - use internal defaults + #define DFTE_PLACEHOLDER_NAME_SIZE DFTE_PLACEHOLDER_NAME_SIZE_DEFAULT +#endif + +/** + * Placeholder types for template substitution + */ +enum class PlaceholderType { + PROGMEM_DATA, // Large PROGMEM data (CSS, JS, base64 images) + PROGMEM_TEMPLATE, // Nested template in PROGMEM + RAM_DATA, // Dynamic RAM data via getter functions + DYNAMIC_TEMPLATE, + CONDITIONAL, + ITERATOR +}; + +/** + * Function pointer types for placeholder data access + */ +typedef const char* (*PlaceholderDataGetter)(); +typedef size_t (*PlaceholderLengthGetter)(const void* data); +typedef const char* (*DynamicTemplateGetter)(void* userData); +typedef size_t (*DynamicTemplateLengthGetter)(const char* data, void* userData); + +enum class ConditionalBranchResult { + SKIP, + TRUE_BRANCH, + FALSE_BRANCH +}; + +typedef ConditionalBranchResult (*ConditionalEvaluator)(void* userData); + +enum class IteratorStepResult { + ITEM_READY, + COMPLETE, + ERROR +}; + +struct IteratorItemView; + +typedef void* (*IteratorOpenHandler)(void* userData); +typedef IteratorStepResult (*IteratorNextHandler)(void* handle, struct IteratorItemView& view); +typedef void (*IteratorCloseHandler)(void* handle); + +struct DynamicTemplateDescriptor { + DynamicTemplateGetter getter; + DynamicTemplateLengthGetter getLength; + void* userData; +}; + +struct ConditionalDescriptor { + ConditionalEvaluator evaluate; + const char* truePlaceholder; + const char* falsePlaceholder; + void* userData; +}; + +/** + * Placeholder definition entry + * Represents a single registered placeholder in the registry + */ +struct PlaceholderEntry { + // Allocated to configured size - matches CONFIG_templatePlaceholderNameSize when DeviceFramework is present + char name[DFTE_PLACEHOLDER_NAME_SIZE]; + PlaceholderType type; + const void* data; + PlaceholderLengthGetter getLength; + + PlaceholderEntry() + : type(PlaceholderType::RAM_DATA), + data(nullptr), + getLength(nullptr) { + name[0] = '\0'; + } +}; + +struct IteratorItemView { + const char* templateData; + size_t templateLength; + bool templateIsProgmem; + const PlaceholderEntry* placeholders; + size_t placeholderCount; +}; + +struct IteratorDescriptor { + IteratorOpenHandler open; + IteratorNextHandler next; + IteratorCloseHandler close; + void* userData; +}; + +/** + * Rendering context types - what kind of thing are we currently rendering? + */ +enum class RenderingContextType { + TEMPLATE, // Rendering a template (contains placeholders) + PLACEHOLDER_DATA, // Rendering a data placeholder (PROGMEM_DATA, RAM_DATA) + PLACEHOLDER_TEMPLATE, // Rendering a template placeholder (resolved to template) + PLACEHOLDER_DYNAMIC_TEMPLATE, + PLACEHOLDER_CONDITIONAL, + PLACEHOLDER_ITERATOR +}; + +/** + * Unified rendering context entry + * Represents any active rendering context (template or placeholder) + * Note: Uses PlaceholderEntry which is defined above + */ +struct RenderingContext { + RenderingContextType type; + const char* name; // Placeholder name or template identifier + + // Type-specific data (using union to save memory) + union { + // TEMPLATE context + struct { + const char* templateData; // Pointer to PROGMEM or RAM template data (not a copy) + size_t templateLen; // Length of template in bytes + bool isProgmem; // Storage location flag + size_t position; // Current position in template + size_t bufferPos; // Current position in buffer + size_t bufferLen; // Current buffer length + size_t bufferOffset; // Offset in template where buffer starts + const PlaceholderEntry* iteratorPlaceholders; + size_t iteratorPlaceholderCount; + } templateCtx; + + // PLACEHOLDER_DATA context + struct { + const PlaceholderEntry* entry; + size_t offset; // Current offset in data + } data; + + // PLACEHOLDER_TEMPLATE context + struct { + const PlaceholderEntry* entry; + // Template state is stored in nested TEMPLATE context + } templatePlaceholder; + + struct { + const PlaceholderEntry* entry; + size_t offset; + const char* templateData; + size_t templateLength; + } dynamicTemplate; + + struct { + const PlaceholderEntry* entry; + const ConditionalDescriptor* descriptor; + bool branchResolved; + const char* delegateName; + const PlaceholderEntry* delegateEntry; + } conditional; + + struct { + const PlaceholderEntry* entry; + const IteratorDescriptor* descriptor; + void* handle; + bool initialized; + bool handleOpen; + } iterator; + } context; + + RenderingContext() + : type(RenderingContextType::TEMPLATE), name(nullptr) { + memset(&context, 0, sizeof(context)); + } +}; + +/** + * Template rendering state + */ +enum class TemplateRenderState { + TEXT, // Reading normal text in current context + BUILDING_PLACEHOLDER, // Building placeholder name between % and % + RENDERING_CONTEXT, // Rendering current context (unified for all context types) + COMPLETE, // All rendering complete + ERROR // Error state +}; + +#endif // DEVICEFRAMEWORK_TEMPLATE_TYPES_H + diff --git a/include/TemplateEngine.h b/include/TemplateEngine.h new file mode 100644 index 0000000..8164bf3 --- /dev/null +++ b/include/TemplateEngine.h @@ -0,0 +1,55 @@ +#ifndef TEMPLATE_ENGINE_H +#define TEMPLATE_ENGINE_H + +/** + * DeviceFramework Template Engine + * Main public API header - include this in your projects + * + * Memory-efficient streaming template renderer for ESP8266/ESP32 + * Supports chunked rendering with PROGMEM templates and nested template support + * + * Usage: + * 1. Create a PlaceholderRegistry and register your placeholders + * 2. Create a TemplateContext + * 3. Set the registry on the context + * 4. Initialize with your template + * 5. Call renderNextChunk() repeatedly until complete + * + * Example: + * DeviceFrameworkPlaceholderRegistry registry; + * registry.registerProgmemData("%CSS%", my_css); + * + * DeviceFrameworkTemplateContext ctx; + * ctx.setRegistry(®istry); + * DeviceFrameworkTemplateRenderer::initializeContext(ctx, my_template); + * + * uint8_t buffer[512]; + * while (!DeviceFrameworkTemplateRenderer::isComplete(ctx)) { + * size_t written = DeviceFrameworkTemplateRenderer::renderNextChunk(ctx, buffer, sizeof(buffer)); + * server->sendContent((const char*)buffer, written); + * } + */ + +// Type definitions first +#include "DeviceFrameworkTemplateTypes.h" + +// Core components +#include "DeviceFrameworkTemplateRenderer.h" +#include "DeviceFrameworkTemplateContext.h" +#include "DeviceFrameworkPlaceholderRegistry.h" +#include "DeviceFrameworkTemplateEngineDebug.h" + +// Type aliases for convenience +using TemplateRenderer = DeviceFrameworkTemplateRenderer; +using TemplateContext = DeviceFrameworkTemplateContext; +using PlaceholderRegistry = DeviceFrameworkPlaceholderRegistry; + +// Use new type names +using PlaceholderType = PlaceholderType; +using PlaceholderEntry = PlaceholderEntry; +using RenderingContextType = RenderingContextType; +using RenderingContext = RenderingContext; +using TemplateRenderState = TemplateRenderState; + +#endif // TEMPLATE_ENGINE_H + diff --git a/library.json b/library.json new file mode 100644 index 0000000..0dceb86 --- /dev/null +++ b/library.json @@ -0,0 +1,33 @@ +{ + "name": "DeviceFrameworkTemplateEngine", + "version": "1.0.0", + "description": "Memory-efficient streaming template engine for ESP8266/ESP32 with chunked rendering support. Designed for embedded web interfaces with PROGMEM template support.", + "keywords": [ + "template", + "engine", + "streaming", + "chunked", + "embedded", + "esp8266", + "esp32", + "html", + "rendering", + "low-memory", + "progmem", + "web-interface" + ], + "authors": [ + { + "name": "Alex Hope-O'Connor", + "email": "alex.hope.oconnor@pomonaqld.au" + } + ], + "license": "MIT", + "frameworks": "arduino", + "platforms": ["espressif8266", "espressif32"], + "dependencies": {}, + "export": { + "include": ["include"] + } +} + diff --git a/platformio.ini b/platformio.ini new file mode 100644 index 0000000..cf07284 --- /dev/null +++ b/platformio.ini @@ -0,0 +1,6 @@ +[env:test_template_engine] +platform = espressif8266 +board = d1_mini +framework = arduino +test_framework = unity +test_build_src = yes diff --git a/src/DeviceFrameworkPlaceholderRegistry.cpp b/src/DeviceFrameworkPlaceholderRegistry.cpp new file mode 100644 index 0000000..20955ae --- /dev/null +++ b/src/DeviceFrameworkPlaceholderRegistry.cpp @@ -0,0 +1,294 @@ +#include "DeviceFrameworkPlaceholderRegistry.h" +#include "DeviceFrameworkTemplateEngineDebug.h" +#include + +DeviceFrameworkPlaceholderRegistry::DeviceFrameworkPlaceholderRegistry(uint16_t maxPlaceholders) + : maxPlaceholders(maxPlaceholders), count(0) { + placeholders = new PlaceholderEntry[maxPlaceholders]; + memset(placeholders, 0, sizeof(PlaceholderEntry) * maxPlaceholders); +} + +DeviceFrameworkPlaceholderRegistry::~DeviceFrameworkPlaceholderRegistry() { + if (placeholders) { + delete[] placeholders; + placeholders = nullptr; + } +} + +bool DeviceFrameworkPlaceholderRegistry::registerProgmemData(const char* name, const char* progmemData) { + if (count >= maxPlaceholders) { + DFTE_LOG_ERROR("Placeholder registry full, cannot register: " + String(name)); + return false; + } + + if (!validatePlaceholderName(name)) { + return false; + } + + // Check for duplicates + if (getPlaceholder(name) != nullptr) { + DFTE_LOG_WARN("Placeholder already registered: " + String(name)); + // Continue anyway - last registration wins + } + + PlaceholderEntry& entry = placeholders[count]; + strncpy(entry.name, name, sizeof(entry.name) - 1); + entry.name[sizeof(entry.name) - 1] = '\0'; + entry.type = PlaceholderType::PROGMEM_DATA; + entry.data = progmemData; + entry.getLength = getProgmemLength; + + count++; + return true; +} + +bool DeviceFrameworkPlaceholderRegistry::registerProgmemTemplate(const char* name, const char* progmemTemplate) { + if (count >= maxPlaceholders) { + DFTE_LOG_ERROR("Placeholder registry full, cannot register: " + String(name)); + return false; + } + + if (!validatePlaceholderName(name)) { + return false; + } + + PlaceholderEntry& entry = placeholders[count]; + strncpy(entry.name, name, sizeof(entry.name) - 1); + entry.name[sizeof(entry.name) - 1] = '\0'; + entry.type = PlaceholderType::PROGMEM_TEMPLATE; + entry.data = progmemTemplate; + entry.getLength = getProgmemLength; + + count++; + return true; +} + +bool DeviceFrameworkPlaceholderRegistry::registerRamData(const char* name, PlaceholderDataGetter getter) { + if (count >= maxPlaceholders) { + DFTE_LOG_ERROR("Placeholder registry full, cannot register: " + String(name)); + return false; + } + + if (!validatePlaceholderName(name)) { + return false; + } + + if (getter == nullptr) { + DFTE_LOG_ERROR("Cannot register RAM_DATA placeholder with null getter: " + String(name)); + return false; + } + + PlaceholderEntry& entry = placeholders[count]; + strncpy(entry.name, name, sizeof(entry.name) - 1); + entry.name[sizeof(entry.name) - 1] = '\0'; + entry.type = PlaceholderType::RAM_DATA; + entry.data = (const void*)getter; + entry.getLength = getRamLength; + + count++; + return true; +} + +bool DeviceFrameworkPlaceholderRegistry::registerDynamicTemplate(const char* name, const DynamicTemplateDescriptor* descriptor) { + if (count >= maxPlaceholders) { + DFTE_LOG_ERROR("Placeholder registry full, cannot register: " + String(name)); + return false; + } + + if (!validatePlaceholderName(name)) { + return false; + } + + if (descriptor == nullptr || descriptor->getter == nullptr) { + DFTE_LOG_ERROR("Invalid dynamic template descriptor for placeholder: " + String(name)); + return false; + } + + PlaceholderEntry& entry = placeholders[count]; + strncpy(entry.name, name, sizeof(entry.name) - 1); + entry.name[sizeof(entry.name) - 1] = '\0'; + entry.type = PlaceholderType::DYNAMIC_TEMPLATE; + entry.data = descriptor; + entry.getLength = nullptr; + + count++; + return true; +} + +bool DeviceFrameworkPlaceholderRegistry::registerConditional(const char* name, const ConditionalDescriptor* descriptor) { + if (count >= maxPlaceholders) { + DFTE_LOG_ERROR("Placeholder registry full, cannot register: " + String(name)); + return false; + } + + if (!validatePlaceholderName(name)) { + return false; + } + + if (descriptor == nullptr || descriptor->evaluate == nullptr) { + DFTE_LOG_ERROR("Invalid conditional descriptor for placeholder: " + String(name)); + return false; + } + + PlaceholderEntry& entry = placeholders[count]; + strncpy(entry.name, name, sizeof(entry.name) - 1); + entry.name[sizeof(entry.name) - 1] = '\0'; + entry.type = PlaceholderType::CONDITIONAL; + entry.data = descriptor; + entry.getLength = nullptr; + + count++; + return true; +} + +bool DeviceFrameworkPlaceholderRegistry::registerIterator(const char* name, const IteratorDescriptor* descriptor) { + if (count >= maxPlaceholders) { + DFTE_LOG_ERROR("Placeholder registry full, cannot register: " + String(name)); + return false; + } + + if (!validatePlaceholderName(name)) { + return false; + } + + if (descriptor == nullptr || descriptor->next == nullptr) { + DFTE_LOG_ERROR("Invalid iterator descriptor for placeholder: " + String(name)); + return false; + } + + PlaceholderEntry& entry = placeholders[count]; + strncpy(entry.name, name, sizeof(entry.name) - 1); + entry.name[sizeof(entry.name) - 1] = '\0'; + entry.type = PlaceholderType::ITERATOR; + entry.data = descriptor; + entry.getLength = nullptr; + + count++; + return true; +} + +void DeviceFrameworkPlaceholderRegistry::clear() { + count = 0; + memset(placeholders, 0, sizeof(PlaceholderEntry) * maxPlaceholders); +} + +bool DeviceFrameworkPlaceholderRegistry::validatePlaceholderName(const char* name) const { + if (!name) { + DFTE_LOG_ERROR("Placeholder name is null"); + return false; + } + + size_t nameLen = strlen(name); + if (nameLen >= MAX_PLACEHOLDER_NAME_SIZE) { + DFTE_LOG_ERROR("Placeholder name too long: " + String(name) + " (max: " + String(MAX_PLACEHOLDER_NAME_SIZE) + ")"); + return false; + } + + if (nameLen >= sizeof(PlaceholderEntry::name)) { + // This shouldn't happen if MAX_PLACEHOLDER_NAME_SIZE is <= sizeof(PlaceholderEntry::name) + DFTE_LOG_ERROR("Placeholder name exceeds entry buffer size"); + return false; + } + + return true; +} + +const PlaceholderEntry* DeviceFrameworkPlaceholderRegistry::getPlaceholder(const char* name) const { + if (name == nullptr) return nullptr; + + for (int i = count - 1; i >= 0; i--) { + if (strcmp(placeholders[i].name, name) == 0) { + return &placeholders[i]; + } + } + return nullptr; +} + +size_t DeviceFrameworkPlaceholderRegistry::renderPlaceholder(const PlaceholderEntry* entry, size_t offset, + uint8_t* buffer, size_t maxLen) const { + if (entry == nullptr || maxLen == 0) { + return 0; + } + + switch (entry->type) { + case PlaceholderType::PROGMEM_DATA: + case PlaceholderType::PROGMEM_TEMPLATE: + return copyProgmemData((const char*)entry->data, offset, buffer, maxLen); + + case PlaceholderType::RAM_DATA: + return copyRamData((PlaceholderDataGetter)entry->data, offset, buffer, maxLen); + + case PlaceholderType::DYNAMIC_TEMPLATE: + case PlaceholderType::CONDITIONAL: + case PlaceholderType::ITERATOR: + // Dynamic template, conditional, and iterator content are handled directly by the renderer + return 0; + + default: + return 0; + } +} + +size_t DeviceFrameworkPlaceholderRegistry::getProgmemLength(const void* data) { + if (data == nullptr) return 0; + return strlen_P((const char*)data); +} + +size_t DeviceFrameworkPlaceholderRegistry::getRamLength(const void* data) { + if (data == nullptr) return 0; + + PlaceholderDataGetter getter = (PlaceholderDataGetter)data; + const char* str = getter(); + if (str == nullptr) { + return 0; + } + return strlen(str); +} + +size_t DeviceFrameworkPlaceholderRegistry::getDynamicTemplateLength(const DynamicTemplateDescriptor* descriptor, const char* templateData) { + if (descriptor == nullptr || templateData == nullptr) { + return 0; + } + + if (descriptor->getLength) { + return descriptor->getLength(templateData, descriptor->userData); + } + + return strlen(templateData); +} + +size_t DeviceFrameworkPlaceholderRegistry::copyProgmemData(const char* source, size_t offset, uint8_t* dest, size_t maxLen) { + if (source == nullptr || maxLen == 0) return 0; + + size_t dataLen = strlen_P(source); + if (offset >= dataLen) return 0; + + size_t remaining = dataLen - offset; + // Use configurable chunk size - matches CONFIG_templateProgmemChunkSize when DeviceFramework is present + constexpr size_t MAX_CHUNK = DFTE_PROGMEM_CHUNK_SIZE; + size_t chunkSize = min(min(maxLen, remaining), MAX_CHUNK); + + memcpy_P(dest, source + offset, chunkSize); + return chunkSize; +} + +size_t DeviceFrameworkPlaceholderRegistry::copyRamData(PlaceholderDataGetter getter, size_t offset, uint8_t* dest, size_t maxLen) { + if (getter == nullptr || maxLen == 0) return 0; + + const char* data = getter(); + if (data == nullptr) { + return 0; + } + size_t dataLen = strlen(data); + + if (offset >= dataLen) return 0; + + size_t remaining = dataLen - offset; + // Use configurable chunk size - matches CONFIG_templateRamChunkSize when DeviceFramework is present + constexpr size_t MAX_CHUNK = DFTE_RAM_CHUNK_SIZE; + size_t chunkSize = min(min(maxLen, remaining), MAX_CHUNK); + + memcpy(dest, data + offset, chunkSize); + return chunkSize; +} + diff --git a/src/DeviceFrameworkTemplateContext.cpp b/src/DeviceFrameworkTemplateContext.cpp new file mode 100644 index 0000000..47f18f4 --- /dev/null +++ b/src/DeviceFrameworkTemplateContext.cpp @@ -0,0 +1,260 @@ +#include "DeviceFrameworkTemplateContext.h" +#include "DeviceFrameworkTemplateEngineDebug.h" + +DeviceFrameworkTemplateContext::DeviceFrameworkTemplateContext() + : state(TemplateRenderState::TEXT), renderingDepth(0), placeholderPos(0), + bufferPos(0), bufferLen(0), bufferOffset(0), + registry(nullptr), + totalBytesProcessed(0), startTime(0) { + memset(placeholderName, 0, sizeof(placeholderName)); + memset(renderingStack, 0, sizeof(renderingStack)); +} + +void DeviceFrameworkTemplateContext::reset() { + state = TemplateRenderState::TEXT; + renderingDepth = 0; + placeholderPos = 0; + bufferPos = 0; + bufferLen = 0; + bufferOffset = 0; + totalBytesProcessed = 0; + startTime = millis(); + memset(placeholderName, 0, sizeof(placeholderName)); + memset(renderingStack, 0, sizeof(renderingStack)); +} + +// Unified stack management methods +bool DeviceFrameworkTemplateContext::pushContext(RenderingContextType type, const char* name) { + if (renderingDepth >= MAX_RENDERING_DEPTH) { + DFTE_LOG_ERROR("Rendering stack overflow! Depth=" + String(renderingDepth)); + state = TemplateRenderState::ERROR; + return false; + } + + // Save current buffer state if we're currently in a template context + if (renderingDepth > 0) { + RenderingContext* currentCtx = getCurrentContext(); + if (currentCtx && currentCtx->type == RenderingContextType::TEMPLATE) { + // Save buffer state to current template context + currentCtx->context.templateCtx.bufferPos = bufferPos; + currentCtx->context.templateCtx.bufferLen = bufferLen; + currentCtx->context.templateCtx.bufferOffset = bufferOffset; + } + } + + RenderingContext& ctx = renderingStack[renderingDepth]; + ctx.type = type; + ctx.name = name; + + // Initialize buffer state for new template context + if (type == RenderingContextType::TEMPLATE) { + ctx.context.templateCtx.bufferPos = 0; + ctx.context.templateCtx.bufferLen = 0; + ctx.context.templateCtx.bufferOffset = 0; + ctx.context.templateCtx.iteratorPlaceholders = nullptr; + ctx.context.templateCtx.iteratorPlaceholderCount = 0; + bufferPos = 0; + bufferLen = 0; + bufferOffset = 0; + } + + renderingDepth++; + return true; +} + +void DeviceFrameworkTemplateContext::popContext() { + if (renderingDepth <= 0) { + DFTE_LOG_ERROR("Rendering stack underflow!"); + state = TemplateRenderState::ERROR; + return; + } + + renderingDepth--; + RenderingContext& ctx = renderingStack[renderingDepth]; + + if (ctx.type == RenderingContextType::PLACEHOLDER_ITERATOR) { + const IteratorDescriptor* descriptor = ctx.context.iterator.descriptor; + if (descriptor && descriptor->close && ctx.context.iterator.handleOpen) { + descriptor->close(ctx.context.iterator.handle); + } + } + + // Restore buffer state from parent template context if it exists + if (renderingDepth > 0) { + RenderingContext* parentCtx = getCurrentContext(); + if (parentCtx && parentCtx->type == RenderingContextType::TEMPLATE) { + auto& parentTemplateCtx = parentCtx->context.templateCtx; + // Invalidate shared buffer so parent refill starts fresh + bufferPos = 0; + bufferLen = 0; + bufferOffset = parentTemplateCtx.position; + + // Clear cached buffer state stored on the parent template context + parentTemplateCtx.bufferPos = 0; + parentTemplateCtx.bufferLen = 0; + parentTemplateCtx.bufferOffset = parentTemplateCtx.position; + } else { + // No parent template context, reset buffer state + bufferPos = 0; + bufferLen = 0; + bufferOffset = 0; + } + } else { + // No more contexts, reset buffer state + bufferPos = 0; + bufferLen = 0; + bufferOffset = 0; + } + + // Clear the popped context + memset(&ctx, 0, sizeof(RenderingContext)); +} + +RenderingContext* DeviceFrameworkTemplateContext::getCurrentContext() { + if (renderingDepth == 0) return nullptr; + return &renderingStack[renderingDepth - 1]; +} + +RenderingContext* DeviceFrameworkTemplateContext::getContext(int depth) { + if (depth < 0 || depth >= renderingDepth) return nullptr; + return &renderingStack[depth]; +} + +bool DeviceFrameworkTemplateContext::isRenderingTemplate() const { + if (renderingDepth == 0) return false; + return renderingStack[renderingDepth - 1].type == RenderingContextType::TEMPLATE; +} + +bool DeviceFrameworkTemplateContext::isRenderingPlaceholder() const { + if (renderingDepth == 0) return false; + RenderingContextType type = renderingStack[renderingDepth - 1].type; + return type == RenderingContextType::PLACEHOLDER_DATA || + type == RenderingContextType::PLACEHOLDER_TEMPLATE; +} + +RenderingContextType DeviceFrameworkTemplateContext::getCurrentContextType() const { + if (renderingDepth == 0) return RenderingContextType::TEMPLATE; // Default + return renderingStack[renderingDepth - 1].type; +} + +bool DeviceFrameworkTemplateContext::isComplete() const { + return state == TemplateRenderState::COMPLETE || state == TemplateRenderState::ERROR; +} + +bool DeviceFrameworkTemplateContext::hasError() const { + return state == TemplateRenderState::ERROR; +} + +String DeviceFrameworkTemplateContext::getStateString() const { + 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"; + } +} + +String DeviceFrameworkTemplateContext::getStackTrace() const { + String trace = "Stack trace (depth=" + String(renderingDepth) + "):\n"; + for (int i = 0; i < renderingDepth; i++) { + const RenderingContext& ctx = renderingStack[i]; + String typeStr; + switch (ctx.type) { + case RenderingContextType::TEMPLATE: typeStr = "TEMPLATE"; break; + case RenderingContextType::PLACEHOLDER_DATA: typeStr = "PLACEHOLDER_DATA"; break; + case RenderingContextType::PLACEHOLDER_TEMPLATE: typeStr = "PLACEHOLDER_TEMPLATE"; break; + default: typeStr = "UNKNOWN"; break; + } + trace += " [" + String(i) + "] " + String(ctx.name) + " (type=" + typeStr + ")"; + if (ctx.type == RenderingContextType::TEMPLATE) { + trace += " at pos " + String(ctx.context.templateCtx.position); + } else if (ctx.type == RenderingContextType::PLACEHOLDER_DATA) { + trace += " at offset " + String(ctx.context.data.offset); + } + trace += "\n"; + } + return trace; +} + +bool DeviceFrameworkTemplateContext::refillBuffer() { + RenderingContext* currentCtx = getCurrentContext(); + if (!currentCtx || currentCtx->type != RenderingContextType::TEMPLATE) { + return false; + } + + auto& templateCtx = currentCtx->context.templateCtx; + if (templateCtx.position >= templateCtx.templateLen) { + return false; + } + + bufferLen = min(BUFFER_SIZE, templateCtx.templateLen - templateCtx.position); + + if (bufferLen > 0) { + if (templateCtx.isProgmem) { + memcpy_P(readBuffer, templateCtx.templateData + templateCtx.position, bufferLen); + } else { + memcpy(readBuffer, templateCtx.templateData + templateCtx.position, bufferLen); + } + bufferPos = 0; + bufferOffset = templateCtx.position; + // Save buffer state to template context + templateCtx.bufferPos = bufferPos; + templateCtx.bufferLen = bufferLen; + templateCtx.bufferOffset = bufferOffset; + // Don't advance templateCtx.position here - it will be updated as we read characters + return true; + } + + return false; +} + +char DeviceFrameworkTemplateContext::getNextChar() { + RenderingContext* currentCtx = getCurrentContext(); + if (!currentCtx || currentCtx->type != RenderingContextType::TEMPLATE) { + return '\0'; + } + + // Restore buffer state from template context if needed + auto& templateCtx = currentCtx->context.templateCtx; + if (bufferPos == 0 && bufferLen == 0 && templateCtx.bufferLen > 0) { + // Buffer state might be stale, restore from context + bufferPos = templateCtx.bufferPos; + bufferLen = templateCtx.bufferLen; + bufferOffset = templateCtx.bufferOffset; + } + + if (bufferPos >= bufferLen) { + if (!refillBuffer()) { + return '\0'; // End of template + } + } + char c = readBuffer[bufferPos++]; + // Update template position to reflect current position + templateCtx.position = bufferOffset + bufferPos; + // Save buffer state to template context + templateCtx.bufferPos = bufferPos; + templateCtx.bufferLen = bufferLen; + templateCtx.bufferOffset = bufferOffset; + return c; +} + +size_t DeviceFrameworkTemplateContext::getAvailableBytes() const { + return bufferLen - bufferPos; +} + +bool DeviceFrameworkTemplateContext::hasMoreData() const { + RenderingContext* currentCtx = const_cast(this)->getCurrentContext(); + if (!currentCtx || currentCtx->type != RenderingContextType::TEMPLATE) { + return false; + } + const auto& templateCtx = currentCtx->context.templateCtx; + return templateCtx.position < templateCtx.templateLen || bufferPos < bufferLen; +} + +void DeviceFrameworkTemplateContext::resetPlaceholder() { + placeholderPos = 0; + memset(placeholderName, 0, sizeof(placeholderName)); +} + diff --git a/src/DeviceFrameworkTemplateEngineDebug.cpp b/src/DeviceFrameworkTemplateEngineDebug.cpp new file mode 100644 index 0000000..8a413ad --- /dev/null +++ b/src/DeviceFrameworkTemplateEngineDebug.cpp @@ -0,0 +1,28 @@ +#include "DeviceFrameworkTemplateEngineDebug.h" + +// Global logger pointer - starts as nullptr (disabled by default) +DeviceFrameworkTemplateEngineLogger* deviceFrameworkTemplateEngineLogger = nullptr; + +// ============================================================================ +// LOGGING CONFIGURATION IMPLEMENTATIONS +// ============================================================================ + +bool deviceFrameworkTemplateEngineEnableLogging(DeviceFrameworkTemplateEngineLogger* logger) { + if (logger == nullptr) { + return false; + } + deviceFrameworkTemplateEngineLogger = logger; + return true; +} + +void deviceFrameworkTemplateEngineDisableLogging() { + deviceFrameworkTemplateEngineLogger = nullptr; +} + +bool deviceFrameworkTemplateEngineIsLoggingEnabled() { + return deviceFrameworkTemplateEngineLogger != nullptr; +} + +DeviceFrameworkTemplateEngineLogger* deviceFrameworkTemplateEngineGetLogger() { + return deviceFrameworkTemplateEngineLogger; +} \ No newline at end of file diff --git a/src/DeviceFrameworkTemplateRenderer.cpp b/src/DeviceFrameworkTemplateRenderer.cpp new file mode 100644 index 0000000..0af4853 --- /dev/null +++ b/src/DeviceFrameworkTemplateRenderer.cpp @@ -0,0 +1,860 @@ +#include "DeviceFrameworkTemplateRenderer.h" +#include "DeviceFrameworkTemplateEngineDebug.h" +#include +#include + +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(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(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(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(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(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(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(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: + return makeState(TemplateRenderState::RENDERING_CONTEXT, true); + + 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 = entry->getLength(entry->data); + 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; + uint8_t* writePtr = buffer; + size_t remaining = maxLen; + + while (remaining > 0 && !ctx.isComplete() && !ctx.hasError() && iterations < MAX_ITERATIONS) { + RenderOutcome outcome = renderChunk(ctx, writePtr, remaining); + + 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"); + } + + 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(); + + // 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(); +} + diff --git a/test/test_template_engine/templates/edge_case_templates.h b/test/test_template_engine/templates/edge_case_templates.h new file mode 100644 index 0000000..c73c530 --- /dev/null +++ b/test/test_template_engine/templates/edge_case_templates.h @@ -0,0 +1,66 @@ +#ifndef EDGE_CASE_TEMPLATES_H +#define EDGE_CASE_TEMPLATES_H + +#include + +// Incomplete placeholder (no closing %) +const char PROGMEM incomplete_placeholder_template[] = "Text with % incomplete"; + +// Placeholder with only % +const char PROGMEM empty_placeholder_template[] = "Text with %% text"; + +// Placeholder with % in text (not a placeholder) +const char PROGMEM percent_in_text_template[] = "Text with 50% discount"; + +// Special characters +const char PROGMEM special_chars_template[] = "Line1\nLine2\tTabbed\r\nWindows"; + +// Template with only placeholders (no text) +const char PROGMEM only_placeholders_template[] = "%A%%B%%C%"; + +// Template with newlines and placeholders +const char PROGMEM newlines_template[] = + "Line 1\n" + "%PLACEHOLDER%\n" + "Line 3\n"; + +// Template with tabs and placeholders +const char PROGMEM tabs_template[] = + "Column1\tColumn2\t%PLACEHOLDER%\tColumn4"; + +// Template with mixed whitespace +const char PROGMEM whitespace_template[] = + " Leading spaces\n" + "%PLACEHOLDER%\n" + "Trailing spaces "; + +// Template with unicode-like characters (if supported) +const char PROGMEM unicode_template[] = + "Test: %PLACEHOLDER% with special chars: ©®™"; + +// Template with very long placeholder name (at max length) +const char PROGMEM max_length_placeholder_template[] = "%VERY_LONG_PLACEHOLDER_NAME%"; + +// Template with placeholder name that might exceed max +const char PROGMEM over_length_placeholder_template[] = "%THIS_PLACEHOLDER_NAME_IS_TOO_LONG_FOR_BUFFER%"; + +// Template with multiple incomplete placeholders +const char PROGMEM multiple_incomplete_template[] = + "Text with % incomplete and % another incomplete"; + +// Template with placeholder at very end +const char PROGMEM placeholder_at_end_template[] = "Text%END%"; + +// Template with placeholder at very start +const char PROGMEM placeholder_at_start_template[] = "%START%Text"; + +// Template with only text (no placeholders) +const char PROGMEM only_text_template[] = + "This is a template with only text and no placeholders at all."; + +// Template with escaped-like patterns (not actual escaping, just patterns) +const char PROGMEM escaped_patterns_template[] = + "Text with %% and %PLACEHOLDER% and %%"; + +#endif // EDGE_CASE_TEMPLATES_H + diff --git a/test/test_template_engine/templates/nested_templates.h b/test/test_template_engine/templates/nested_templates.h new file mode 100644 index 0000000..8571bd5 --- /dev/null +++ b/test/test_template_engine/templates/nested_templates.h @@ -0,0 +1,163 @@ +#ifndef NESTED_TEMPLATES_H +#define NESTED_TEMPLATES_H + +#include + +// One level nesting +const char PROGMEM nested_one_level_outer[] = "Outer: %INNER%"; +const char PROGMEM nested_one_level_inner[] = "Inner content"; + +// Two levels nesting +const char PROGMEM nested_two_levels_outer[] = "Level1: %LEVEL2%"; +const char PROGMEM nested_two_levels_mid[] = "Level2: %LEVEL3%"; +const char PROGMEM nested_two_levels_inner[] = "Level3 content"; + +// Three levels nesting +const char PROGMEM nested_three_levels_outer[] = "Outer: %MID%"; +const char PROGMEM nested_three_levels_mid[] = "Mid: %INNER%"; +const char PROGMEM nested_three_levels_inner[] = "Inner: %DEEP%"; +const char PROGMEM nested_three_levels_deep[] = "Deep content"; + +// Four levels nesting +const char PROGMEM nested_four_levels_outer[] = "L1: %L2%"; +const char PROGMEM nested_four_levels_level2[] = "L2: %L3%"; +const char PROGMEM nested_four_levels_level3[] = "L3: %L4%"; +const char PROGMEM nested_four_levels_level4[] = "L4 content"; + +// Large web-style templates to simulate WebInterface-sized payloads (intentionally fuzzed) +const char PROGMEM simulated_web_outer[] = R"rawliteral( + + + + + + %TITLE% + + + +
+ %HEADER% +
+ %SIDEBAR% +
+ %CONTENT% +
+
+ %FOOTER% +
+ + + +)rawliteral"; + +const char PROGMEM simulated_web_header[] = R"rawliteral( +
+
+ +
+

%TITLE%

+

Powering ambient devices

+
+
+ %NAV% +
+)rawliteral"; + +const char PROGMEM simulated_web_nav[] = R"rawliteral( + +)rawliteral"; + +const char PROGMEM simulated_web_sidebar[] = R"rawliteral( + +)rawliteral"; + +const char PROGMEM simulated_web_content[] = R"rawliteral( +
+

Device Overview

+

Preparing device snapshot...

+
+
+

Telemetry

+
+
+

Network

+

SSID: pending

+

IP: pending

+
+
+

MQTT

+

Status: pending

+

Broker: pending

+
+
+

Sensors

+

Temperature: --

+

Humidity: --

+
+
+
+
+

Automation Actions

+ + +
+)rawliteral"; + +const char PROGMEM simulated_web_footer[] = R"rawliteral( +
+ © 2024 Elixir DeviceFramework Labs +
+)rawliteral"; + +const char PROGMEM simulated_web_scripts[] = R"rawliteral( +function queueAction(action) { + window.dispatchEvent(new CustomEvent('device-action', { detail: action })); +} +window.addEventListener('load', () => { + document.body.dataset.ready = 'true'; +}); +)rawliteral"; + +// Nested template with placeholders +const char PROGMEM nested_with_placeholder_outer[] = "Outer: %NESTED%"; +const char PROGMEM nested_with_placeholder_inner[] = "Inner: %PLACEHOLDER%"; + +// Nested template with multiple placeholders +const char PROGMEM nested_multi_outer[] = "%HEADER%\n%CONTENT%\n%FOOTER%"; +const char PROGMEM nested_multi_header[] = "
%TITLE%
"; +const char PROGMEM nested_multi_content[] = "
%BODY%
"; +const char PROGMEM nested_multi_footer[] = "
Footer
"; + +// Deep nesting for stack depth testing +const char PROGMEM deep_nest_level1[] = "L1: %L2%"; +const char PROGMEM deep_nest_level2[] = "L2: %L3%"; +const char PROGMEM deep_nest_level3[] = "L3: %L4%"; +const char PROGMEM deep_nest_level4[] = "L4: %L5%"; +const char PROGMEM deep_nest_level5[] = "L5: %L6%"; +const char PROGMEM deep_nest_level6[] = "L6: %L7%"; +const char PROGMEM deep_nest_level7[] = "L7: %L8%"; +const char PROGMEM deep_nest_level8[] = "L8: %L9%"; +const char PROGMEM deep_nest_level9[] = "L9: %L10%"; +const char PROGMEM deep_nest_level10[] = "L10: %L11%"; +const char PROGMEM deep_nest_level11[] = "L11: %L12%"; +const char PROGMEM deep_nest_level12[] = "L12: %L13%"; +const char PROGMEM deep_nest_level13[] = "L13: %L14%"; +const char PROGMEM deep_nest_level14[] = "L14: %L15%"; +const char PROGMEM deep_nest_level15[] = "L15: %L16%"; +const char PROGMEM deep_nest_level16[] = "L16: content"; + +#endif // NESTED_TEMPLATES_H + diff --git a/test/test_template_engine/templates/placeholder_templates.h b/test/test_template_engine/templates/placeholder_templates.h new file mode 100644 index 0000000..35ddb9d --- /dev/null +++ b/test/test_template_engine/templates/placeholder_templates.h @@ -0,0 +1,38 @@ +#ifndef PLACEHOLDER_TEMPLATES_H +#define PLACEHOLDER_TEMPLATES_H + +#include + +// PROGMEM_DATA placeholder +const char PROGMEM progmem_data_template[] = "CSS: %STYLES%"; + +// PROGMEM_TEMPLATE placeholder +const char PROGMEM nested_template_template[] = "Header: %HEADER%"; + +// RAM_DATA placeholder +const char PROGMEM ram_data_template[] = "Title: %PAGE_TITLE%"; + +// Mixed placeholders +const char PROGMEM mixed_placeholders_template[] = + "%HEADER%\nTitle: %PAGE_TITLE%\n%STYLES%\n%FOOTER%"; + +// Multiple PROGMEM_DATA placeholders +const char PROGMEM multiple_progmem_template[] = + "%CSS%\n%JS%\n%FAVICON%"; + +// Multiple RAM_DATA placeholders +const char PROGMEM multiple_ram_template[] = + "Title: %TITLE%\nSubtitle: %SUBTITLE%\nDescription: %DESC%"; + +// Test data for PROGMEM placeholders +const char PROGMEM test_css_data[] = "body { color: red; }"; +const char PROGMEM test_js_data[] = "console.log('test');"; +const char PROGMEM test_favicon_data[] = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=="; + +// Test templates for nested templates +const char PROGMEM test_header_template[] = "
%LOGO%
"; +const char PROGMEM test_footer_template[] = "
Footer
"; +const char PROGMEM test_logo_template[] = ""; + +#endif // PLACEHOLDER_TEMPLATES_H + diff --git a/test/test_template_engine/templates/simple_templates.h b/test/test_template_engine/templates/simple_templates.h new file mode 100644 index 0000000..f19aa4d --- /dev/null +++ b/test/test_template_engine/templates/simple_templates.h @@ -0,0 +1,35 @@ +#ifndef SIMPLE_TEMPLATES_H +#define SIMPLE_TEMPLATES_H + +#include + +// Empty template +const char PROGMEM empty_template[] = ""; + +// Plain text template +const char PROGMEM plain_text_template[] = "Hello, World!"; + +// Single placeholder +const char PROGMEM single_placeholder_template[] = "Hello, %NAME%!"; + +// Multiple placeholders +const char PROGMEM multiple_placeholders_template[] = + "Title: %TITLE%\nContent: %CONTENT%\nFooter: %FOOTER%"; + +// Placeholder at start +const char PROGMEM placeholder_start_template[] = "%PLACEHOLDER% text"; + +// Placeholder at end +const char PROGMEM placeholder_end_template[] = "text %PLACEHOLDER%"; + +// Consecutive placeholders +const char PROGMEM consecutive_placeholders_template[] = "%A%%B%%C%"; + +// Long text between placeholders +const char PROGMEM long_text_template[] = + "This is a very long text that should test buffer refill logic. " + "It contains many characters and should span multiple buffer fills. " + "%PLACEHOLDER% More text here."; + +#endif // SIMPLE_TEMPLATES_H + diff --git a/test/test_template_engine/test_main.cpp b/test/test_template_engine/test_main.cpp new file mode 100644 index 0000000..0a06cb6 --- /dev/null +++ b/test/test_template_engine/test_main.cpp @@ -0,0 +1,139 @@ +#include +#include +#include +#include "test_main.h" +#include "utils/test_utils.h" +#include "DeviceFrameworkTemplateEngineDebug.h" + +// Logger implementation for tests - uses Serial.println +class TestTemplateEngineLogger : public DeviceFrameworkTemplateEngineLogger { +public: + void error(const String& msg) override { + Serial.print("[DFTE ERROR] "); + Serial.println(msg); + } + + void warn(const String& msg) override { + Serial.print("[DFTE WARN] "); + Serial.println(msg); + } + + void info(const String& msg) override { + Serial.print("[DFTE INFO] "); + Serial.println(msg); + } + + void debug(const String& msg) override { + Serial.print("[DFTE DEBUG] "); + Serial.println(msg); + } +}; + +// Global logger instance +TestTemplateEngineLogger testLogger; + +// Test case array +TestCase tests[] = { + // Group 1: PlaceholderRegistry Tests + TEST_ENTRY(test_placeholder_registry_registration), + TEST_ENTRY(test_placeholder_registry_lookup), + TEST_ENTRY(test_placeholder_registry_rendering), + TEST_ENTRY(test_placeholder_registry_edge_cases), + + // Group 2: TemplateContext Tests + TEST_ENTRY(test_template_context_initialization), + TEST_ENTRY(test_template_context_stack), + TEST_ENTRY(test_template_context_buffer), + TEST_ENTRY(test_template_context_state), + + // Group 3: TemplateRenderer Tests + TEST_ENTRY(test_template_renderer_basic), + TEST_ENTRY(test_template_renderer_chunked), + TEST_ENTRY(test_template_renderer_state_transitions), + TEST_ENTRY(test_template_renderer_placeholders), + TEST_ENTRY(test_template_renderer_nested), + TEST_ENTRY(test_template_renderer_nested_four_levels), + TEST_ENTRY(test_template_renderer_nested_chunk_progress), + TEST_ENTRY(test_template_renderer_large_templates), + TEST_ENTRY(test_template_renderer_parallel_contexts), + TEST_ENTRY(test_template_renderer_root_ram_template), + TEST_ENTRY(test_template_renderer_missing_registry), + TEST_ENTRY(test_template_renderer_dynamic_basic), + TEST_ENTRY(test_template_renderer_dynamic_empty), + TEST_ENTRY(test_template_renderer_dynamic_mutable), + TEST_ENTRY(test_template_renderer_conditional_true_branch), + TEST_ENTRY(test_template_renderer_conditional_false_branch), + TEST_ENTRY(test_template_renderer_conditional_skip), + TEST_ENTRY(test_template_renderer_conditional_missing_delegate), + TEST_ENTRY(test_template_renderer_conditional_nested_iterator), + TEST_ENTRY(test_template_renderer_iterator_basic), + TEST_ENTRY(test_template_renderer_iterator_empty), + TEST_ENTRY(test_template_renderer_iterator_dynamic_items), + TEST_ENTRY(test_template_renderer_iterator_error_cleanup), + + // Group 4: Integration Tests + TEST_ENTRY(test_integration_full_rendering), + TEST_ENTRY(test_integration_memory_efficiency), + TEST_ENTRY(test_integration_multiple_templates), + + // Group 5: Edge Cases + TEST_ENTRY(test_edge_cases_error_handling), + TEST_ENTRY(test_edge_cases_boundary_conditions), + TEST_ENTRY(test_edge_cases_stress), +}; + +const size_t TEST_COUNT = sizeof(tests) / sizeof(TestCase); + +// Test state variables +size_t next_index = 0; +bool begun = false; + +// Test setup and teardown +void setUp(void) { + // No per-test setup needed +} + +void tearDown(void) { + // No per-test teardown needed +} + +// Arduino setup and loop functions (required by framework) +void setup() { + Serial.begin(115200); + delay(2000); // Give time for serial to initialize + + Serial.println("\n[TEST] ============================================="); + Serial.println("[TEST] === DeviceFrameworkTemplateEngine Test Setup ==="); + Serial.println("[TEST] Starting template engine tests..."); + Serial.println("[TEST] ============================================="); + + // Enable template engine debug logging + deviceFrameworkTemplateEngineEnableLogging(&testLogger); + Serial.println("[TEST] Template engine debug logging enabled"); + + UNITY_BEGIN(); // Start Unity test framework + begun = true; // Start tests immediately +} + +void loop() { + // Run one test and return immediately + if (begun && next_index < TEST_COUNT) { + TestCase& t = tests[next_index]; + Serial.print("\n[TEST] ==== Running test: "); + Serial.print(t.name); + Serial.println(" ===="); + UnityDefaultTestRun(t.fn, t.name, t.line); + next_index++; + return; // yield quickly + } + + // All tests completed + if (begun && next_index >= TEST_COUNT) { + Serial.println("\n[TEST] ============================================="); + Serial.println("[TEST] === All tests completed ==="); + Serial.println("[TEST] ============================================="); + UNITY_END(); // prints Unity summary + begun = false; // avoid repeating + } +} + diff --git a/test/test_template_engine/test_main.h b/test/test_template_engine/test_main.h new file mode 100644 index 0000000..658e80f --- /dev/null +++ b/test/test_template_engine/test_main.h @@ -0,0 +1,56 @@ +#ifndef TEST_MAIN_H +#define TEST_MAIN_H + +#include +#include "utils/test_utils.h" + +// Test function declarations +// Group 1: PlaceholderRegistry Tests +void test_placeholder_registry_registration(); +void test_placeholder_registry_lookup(); +void test_placeholder_registry_rendering(); +void test_placeholder_registry_edge_cases(); + +// Group 2: TemplateContext Tests +void test_template_context_initialization(); +void test_template_context_stack(); +void test_template_context_buffer(); +void test_template_context_state(); + +// Group 3: TemplateRenderer Tests +void test_template_renderer_basic(); +void test_template_renderer_chunked(); +void test_template_renderer_state_transitions(); +void test_template_renderer_placeholders(); +void test_template_renderer_nested(); +void test_template_renderer_nested_four_levels(); +void test_template_renderer_nested_chunk_progress(); +void test_template_renderer_large_templates(); +void test_template_renderer_parallel_contexts(); +void test_template_renderer_root_ram_template(); +void test_template_renderer_missing_registry(); +void test_template_renderer_dynamic_basic(); +void test_template_renderer_dynamic_empty(); +void test_template_renderer_dynamic_mutable(); +void test_template_renderer_conditional_true_branch(); +void test_template_renderer_conditional_false_branch(); +void test_template_renderer_conditional_skip(); +void test_template_renderer_conditional_missing_delegate(); +void test_template_renderer_conditional_nested_iterator(); +void test_template_renderer_iterator_basic(); +void test_template_renderer_iterator_empty(); +void test_template_renderer_iterator_dynamic_items(); +void test_template_renderer_iterator_error_cleanup(); + +// Group 4: Integration Tests +void test_integration_full_rendering(); +void test_integration_memory_efficiency(); +void test_integration_multiple_templates(); + +// Group 5: Edge Cases +void test_edge_cases_error_handling(); +void test_edge_cases_boundary_conditions(); +void test_edge_cases_stress(); + +#endif // TEST_MAIN_H + diff --git a/test/test_template_engine/tests/test_edge_cases.cpp b/test/test_template_engine/tests/test_edge_cases.cpp new file mode 100644 index 0000000..cd4cf3d --- /dev/null +++ b/test/test_template_engine/tests/test_edge_cases.cpp @@ -0,0 +1,307 @@ +#include +#include +#include +#include "../templates/simple_templates.h" +#include "../templates/placeholder_templates.h" +#include "../templates/edge_case_templates.h" +#include "../templates/nested_templates.h" +#include "../utils/test_utils.h" + +// Test RAM data getter that returns null +static const char* getNullRamData() { + return nullptr; +} + +// Test RAM data getter that returns empty string +static const char* getEmptyRamData() { + static String empty = ""; + return empty.c_str(); +} + +// Test error handling +void test_edge_cases_error_handling() { + Serial.println("[TEST] Testing error handling..."); + + PlaceholderRegistry registry(10); + TemplateContext ctx; + ctx.setRegistry(®istry); + + // Test render with null template pointer (should handle gracefully) + // Note: initializeContext expects valid PROGMEM pointer, so we'll test with empty template + TemplateRenderer::initializeContext(ctx, empty_template); + String result1 = captureRenderedOutput(ctx); + TEST_ASSERT_EQUAL_STRING_MESSAGE("", result1.c_str(), + "Should handle empty template"); + + // Test render with null registry + ctx.reset(); + ctx.setRegistry(nullptr); + TemplateRenderer::initializeContext(ctx, single_placeholder_template); + String result2 = captureRenderedOutput(ctx); + // Should skip placeholder and render text + TEST_ASSERT_EQUAL_STRING_MESSAGE("Hello, !", result2.c_str(), + "Should handle null registry"); + + // Test RAM getter returning null + registry.registerRamData("%NULL%", getNullRamData); + ctx.reset(); + TemplateRenderer::initializeContext(ctx, single_placeholder_template); + ctx.setRegistry(®istry); + String result3 = captureRenderedOutput(ctx); + // Should handle null return gracefully + TEST_ASSERT_TRUE_MESSAGE(TemplateRenderer::isComplete(ctx), + "Should complete even with null RAM getter"); + + // Test RAM getter returning empty string + registry.registerRamData("%EMPTY%", getEmptyRamData); + ctx.reset(); + TemplateRenderer::initializeContext(ctx, single_placeholder_template); + ctx.setRegistry(®istry); + String result4 = captureRenderedOutput(ctx); + TEST_ASSERT_TRUE_MESSAGE(TemplateRenderer::isComplete(ctx), + "Should complete with empty RAM data"); + + // Test incomplete placeholder (no closing %) + ctx.reset(); + TemplateRenderer::initializeContext(ctx, incomplete_placeholder_template); + ctx.setRegistry(®istry); + String result5 = captureRenderedOutput(ctx); + // Should handle incomplete placeholder gracefully + TEST_ASSERT_TRUE_MESSAGE(TemplateRenderer::isComplete(ctx), + "Should complete even with incomplete placeholder"); + + Serial.println("[TEST] Error handling tests completed successfully"); +} + +// Test boundary conditions +void test_edge_cases_boundary_conditions() { + Serial.println("[TEST] Testing boundary conditions..."); + + PlaceholderRegistry registry(10); + registry.registerProgmemData("%PLACEHOLDER%", test_css_data); + + TemplateContext ctx; + ctx.setRegistry(®istry); + + // Test placeholder at start + // PLACEHOLDER is already registered above, so we can use it + Serial.print("[DEBUG] Edge case - %PLACEHOLDER% registered: "); + Serial.println(registry.getPlaceholder("%PLACEHOLDER%") != nullptr ? "YES" : "NO"); + ctx.reset(); + ctx.setRegistry(®istry); + TemplateRenderer::initializeContext(ctx, placeholder_start_template); + Serial.print("[DEBUG] Edge case template: ["); + Serial.print(placeholder_start_template); + Serial.println("]"); + String result1 = captureRenderedOutput(ctx); + Serial.print("[DEBUG] Edge case result length: "); + Serial.println(result1.length()); + Serial.print("[DEBUG] Edge case result: ["); + Serial.print(result1); + Serial.println("]"); + Serial.print("[DEBUG] Edge case - Contains ' text': "); + Serial.println(result1.indexOf(" text") >= 0 ? "YES" : "NO"); + Serial.print("[DEBUG] Edge case - Contains 'body': "); + Serial.println(result1.indexOf("body") >= 0 ? "YES" : "NO"); + Serial.print("[DEBUG] Edge case - Final state: "); + Serial.println(ctx.getStateString()); + RenderingContext* currentCtx = ctx.getCurrentContext(); + if (currentCtx && currentCtx->type == RenderingContextType::TEMPLATE) { + Serial.print("[DEBUG] Edge case - Template length: "); + Serial.println(currentCtx->context.templateCtx.templateLen); + } + // Should render placeholder and text + TEST_ASSERT_TRUE_MESSAGE(result1.length() > 0, "Should handle placeholder at start"); + // Check for either the placeholder content or the text part + TEST_ASSERT_TRUE_MESSAGE(result1.indexOf(" text") >= 0 || result1.indexOf("body") >= 0, + "Should contain placeholder or text"); + TEST_ASSERT_TRUE_MESSAGE(TemplateRenderer::isComplete(ctx), + "Should complete with placeholder at start"); + + // Test placeholder at end + ctx.reset(); + ctx.setRegistry(®istry); + TemplateRenderer::initializeContext(ctx, placeholder_end_template); + String result2 = captureRenderedOutput(ctx); + TEST_ASSERT_TRUE_MESSAGE(result2.length() > 0, "Should handle placeholder at end"); + TEST_ASSERT_TRUE_MESSAGE(TemplateRenderer::isComplete(ctx), + "Should complete with placeholder at end"); + + // Test only placeholders (no text) + registry.registerProgmemData("%A%", test_css_data); + registry.registerProgmemData("%B%", test_js_data); + registry.registerProgmemData("%C%", test_favicon_data); + ctx.reset(); + TemplateRenderer::initializeContext(ctx, only_placeholders_template); + ctx.setRegistry(®istry); + String result3 = captureRenderedOutput(ctx); + TEST_ASSERT_TRUE_MESSAGE(result3.length() > 0, "Should handle only placeholders"); + TEST_ASSERT_TRUE_MESSAGE(TemplateRenderer::isComplete(ctx), + "Should complete with only placeholders"); + + // Test only text (no placeholders) + ctx.reset(); + TemplateRenderer::initializeContext(ctx, only_text_template); + ctx.setRegistry(®istry); + String result4 = captureRenderedOutput(ctx); + TEST_ASSERT_TRUE_MESSAGE(result4.length() > 0, "Should handle only text"); + TEST_ASSERT_TRUE_MESSAGE(TemplateRenderer::isComplete(ctx), + "Should complete with only text"); + + // Test special characters + ctx.reset(); + TemplateRenderer::initializeContext(ctx, special_chars_template); + ctx.setRegistry(®istry); + String result5 = captureRenderedOutput(ctx); + TEST_ASSERT_TRUE_MESSAGE(result5.length() > 0, "Should handle special characters"); + TEST_ASSERT_TRUE_MESSAGE(TemplateRenderer::isComplete(ctx), + "Should complete with special characters"); + + // Test newlines and placeholders + ctx.reset(); + TemplateRenderer::initializeContext(ctx, newlines_template); + ctx.setRegistry(®istry); + String result6 = captureRenderedOutput(ctx); + TEST_ASSERT_TRUE_MESSAGE(result6.length() > 0, "Should handle newlines"); + TEST_ASSERT_TRUE_MESSAGE(TemplateRenderer::isComplete(ctx), + "Should complete with newlines"); + + // Test empty placeholder (%%) + ctx.reset(); + TemplateRenderer::initializeContext(ctx, empty_placeholder_template); + ctx.setRegistry(®istry); + String result7 = captureRenderedOutput(ctx); + // Should handle empty placeholder gracefully + TEST_ASSERT_TRUE_MESSAGE(TemplateRenderer::isComplete(ctx), + "Should complete with empty placeholder"); + + Serial.println("[TEST] Boundary condition tests completed successfully"); +} + +// Test stress conditions +void test_edge_cases_stress() { + Serial.println("[TEST] Testing stress conditions..."); + + PlaceholderRegistry registry(50); + + // Register many placeholders + for (int i = 0; i < 20; i++) { + char name[10]; + sprintf(name, "%%P%d%%", i); + registry.registerProgmemData(name, test_css_data); + } + + TEST_ASSERT_EQUAL_MESSAGE(20, registry.getCount(), + "Should register many placeholders"); + + // Test template with many placeholders + // Note: PROGMEM templates must be defined at file scope + const char many_placeholders_template[] = + "%P0%%P1%%P2%%P3%%P4%%P5%%P6%%P7%%P8%%P9%" + "%P10%%P11%%P12%%P13%%P14%%P15%%P16%%P17%%P18%%P19%"; + + // All placeholders are already registered above + Serial.print("[DEBUG] Stress test - Registry count: "); + Serial.println(registry.getCount()); + Serial.print("[DEBUG] Stress test - %P0% registered: "); + Serial.println(registry.getPlaceholder("%P0%") != nullptr ? "YES" : "NO"); + Serial.print("[DEBUG] Stress test - %P19% registered: "); + Serial.println(registry.getPlaceholder("%P19%") != nullptr ? "YES" : "NO"); + TemplateContext ctx; + ctx.setRegistry(®istry); + TemplateRenderer::initializeContext(ctx, many_placeholders_template); + Serial.print("[DEBUG] Stress test template length: "); + RenderingContext* currentCtx = ctx.getCurrentContext(); + if (currentCtx && currentCtx->type == RenderingContextType::TEMPLATE) { + Serial.println(currentCtx->context.templateCtx.templateLen); + } else { + Serial.println(0); + } + String result = captureRenderedOutput(ctx); + Serial.print("[DEBUG] Stress test result length: "); + Serial.println(result.length()); + Serial.print("[DEBUG] Stress test result (first 200 chars): ["); + if (result.length() > 200) { + Serial.print(result.substring(0, 200)); + Serial.println("...]"); + } else { + Serial.print(result); + Serial.println("]"); + } + Serial.print("[DEBUG] Stress test - Contains 'body': "); + Serial.println(result.indexOf("body") >= 0 ? "YES" : "NO"); + Serial.print("[DEBUG] Stress test - Final state: "); + Serial.println(ctx.getStateString()); + Serial.print("[DEBUG] Stress test - Is complete: "); + Serial.println(TemplateRenderer::isComplete(ctx) ? "YES" : "NO"); + // All placeholders are registered with test_css_data, so result should contain "body" multiple times + TEST_ASSERT_TRUE_MESSAGE(result.length() > 0, "Should render template with many placeholders"); + // Verify we got some content (all placeholders render the same CSS data) + TEST_ASSERT_TRUE_MESSAGE(result.indexOf("body") >= 0 || result.length() > 20, + "Should contain rendered placeholder data"); + TEST_ASSERT_TRUE_MESSAGE(TemplateRenderer::isComplete(ctx), + "Should complete with many placeholders"); + + // Test very long template + // Register placeholder for long_text_template (if not already registered) + if (registry.getPlaceholder("%PLACEHOLDER%") == nullptr) { + registry.registerProgmemData("%PLACEHOLDER%", test_css_data); + } + ctx.reset(); + ctx.setRegistry(®istry); + TemplateRenderer::initializeContext(ctx, long_text_template); + String result2 = captureRenderedOutput(ctx); + + TEST_ASSERT_TRUE_MESSAGE(result2.length() > 0, "Should render very long template"); + TEST_ASSERT_TRUE_MESSAGE(TemplateRenderer::isComplete(ctx), + "Should complete very long template"); + + // Test rendering with very small buffer repeatedly + ctx.reset(); + ctx.setRegistry(®istry); + TemplateRenderer::initializeContext(ctx, long_text_template); + + uint8_t tinyBuffer[1]; + int iterations = 0; + while (!TemplateRenderer::isComplete(ctx) && iterations < 10000) { + TemplateRenderer::renderNextChunk(ctx, tinyBuffer, sizeof(tinyBuffer)); + iterations++; + } + + TEST_ASSERT_TRUE_MESSAGE(TemplateRenderer::isComplete(ctx), + "Should complete even with tiny buffer"); + TEST_ASSERT_LESS_THAN_MESSAGE(10000, iterations, + "Should complete in reasonable iterations"); + + // Test stack depth near limit + registry.clear(); + registry.registerProgmemTemplate("%L2%", deep_nest_level2); + registry.registerProgmemTemplate("%L3%", deep_nest_level3); + registry.registerProgmemTemplate("%L4%", deep_nest_level4); + registry.registerProgmemTemplate("%L5%", deep_nest_level5); + registry.registerProgmemTemplate("%L6%", deep_nest_level6); + registry.registerProgmemTemplate("%L7%", deep_nest_level7); + registry.registerProgmemTemplate("%L8%", deep_nest_level8); + registry.registerProgmemTemplate("%L9%", deep_nest_level9); + registry.registerProgmemTemplate("%L10%", deep_nest_level10); + registry.registerProgmemTemplate("%L11%", deep_nest_level11); + registry.registerProgmemTemplate("%L12%", deep_nest_level12); + registry.registerProgmemTemplate("%L13%", deep_nest_level13); + registry.registerProgmemTemplate("%L14%", deep_nest_level14); + registry.registerProgmemTemplate("%L15%", deep_nest_level15); + registry.registerProgmemTemplate("%L16%", deep_nest_level16); + + ctx.reset(); + ctx.setRegistry(®istry); + TemplateRenderer::initializeContext(ctx, deep_nest_level1); + String result3 = captureRenderedOutput(ctx); + + TEST_ASSERT_TRUE_MESSAGE(result3.length() > 0, "Should render deep nested template"); + TEST_ASSERT_TRUE_MESSAGE(TemplateRenderer::isComplete(ctx), + "Should complete deep nested template"); + TEST_ASSERT_LESS_OR_EQUAL_MESSAGE(TemplateContext::MAX_RENDERING_DEPTH, ctx.renderingDepth, + "Rendering depth should not exceed MAX_RENDERING_DEPTH"); + + Serial.println("[TEST] Stress condition tests completed successfully"); +} + diff --git a/test/test_template_engine/tests/test_integration.cpp b/test/test_template_engine/tests/test_integration.cpp new file mode 100644 index 0000000..fd77bec --- /dev/null +++ b/test/test_template_engine/tests/test_integration.cpp @@ -0,0 +1,198 @@ +#include +#include +#include +#include "../templates/simple_templates.h" +#include "../templates/placeholder_templates.h" +#include "../templates/nested_templates.h" +#include "../utils/test_utils.h" + +// Test RAM data getters +static String testTitle = "Test Title"; +static String testSubtitle = "Test Subtitle"; +static String testDesc = "Test Description"; + +static const char* getTestTitle() { return testTitle.c_str(); } +static const char* getTestSubtitle() { return testSubtitle.c_str(); } +static const char* getTestDesc() { return testDesc.c_str(); } + +// Test full rendering scenarios +void test_integration_full_rendering() { + Serial.println("[TEST] Testing full rendering scenarios..."); + + PlaceholderRegistry registry(20); + + // Register all placeholder types + // Note: mixed_placeholders_template uses %STYLES%, %PAGE_TITLE%, %HEADER%, %FOOTER% + registry.registerProgmemData("%STYLES%", test_css_data); + registry.registerProgmemData("%JS%", test_js_data); + registry.registerProgmemData("%FAVICON%", test_favicon_data); + registry.registerProgmemTemplate("%HEADER%", test_header_template); + registry.registerProgmemTemplate("%FOOTER%", test_footer_template); + registry.registerRamData("%PAGE_TITLE%", getTestTitle); + registry.registerRamData("%SUBTITLE%", getTestSubtitle); + registry.registerRamData("%DESC%", getTestDesc); + + TemplateContext ctx; + ctx.setRegistry(®istry); + Serial.print("[DEBUG] Registry count: "); + Serial.println(registry.getCount()); + Serial.print("[DEBUG] %STYLES% registered: "); + Serial.println(registry.getPlaceholder("%STYLES%") != nullptr ? "YES" : "NO"); + Serial.print("[DEBUG] %PAGE_TITLE% registered: "); + Serial.println(registry.getPlaceholder("%PAGE_TITLE%") != nullptr ? "YES" : "NO"); + Serial.print("[DEBUG] %HEADER% registered: "); + Serial.println(registry.getPlaceholder("%HEADER%") != nullptr ? "YES" : "NO"); + Serial.print("[DEBUG] %FOOTER% registered: "); + Serial.println(registry.getPlaceholder("%FOOTER%") != nullptr ? "YES" : "NO"); + TemplateRenderer::initializeContext(ctx, mixed_placeholders_template); + String result = captureRenderedOutput(ctx); + Serial.print("[DEBUG] Mixed result length: "); + Serial.println(result.length()); + Serial.print("[DEBUG] Mixed result: ["); + Serial.print(result); + Serial.println("]"); + Serial.print("[DEBUG] Contains 'Header:': "); + Serial.println(result.indexOf("Header:") >= 0 ? "YES" : "NO"); + Serial.print("[DEBUG] Contains 'header': "); + Serial.println(result.indexOf("header") >= 0 ? "YES" : "NO"); + Serial.print("[DEBUG] Contains 'body': "); + Serial.println(result.indexOf("body") >= 0 ? "YES" : "NO"); + Serial.print("[DEBUG] Contains 'Title:': "); + Serial.println(result.indexOf("Title:") >= 0 ? "YES" : "NO"); + TEST_ASSERT_TRUE_MESSAGE(result.length() > 0, "Should render template with all placeholder types"); + // Check for rendered content - PROGMEM data is "body { color: red; }" + TEST_ASSERT_TRUE_MESSAGE(result.indexOf("Header:") >= 0 || result.indexOf("header") >= 0 || result.indexOf("body") >= 0 || result.indexOf("Title:") >= 0, + "Should contain rendered content"); + TEST_ASSERT_TRUE_MESSAGE(TemplateRenderer::isComplete(ctx), + "Should be complete after rendering"); + + // Test real-world HTML-like template structure + // Note: PROGMEM templates must be defined at file scope, not as local variables + // Using a simple template string instead + // Register placeholders for HTML template + registry.registerRamData("%TITLE%", getTestTitle); + registry.registerProgmemData("%CSS%", test_css_data); + + const char html_template[] = + "\n" + "\n" + "\n" + " %TITLE%\n" + " \n" + "\n" + "\n" + " %HEADER%\n" + "
%DESC%
\n" + " %FOOTER%\n" + " \n" + "\n" + ""; + + ctx.reset(); + ctx.setRegistry(®istry); + TemplateRenderer::initializeContext(ctx, html_template); + String htmlResult = captureRenderedOutput(ctx); + + TEST_ASSERT_TRUE_MESSAGE(htmlResult.length() > 0, "Should render HTML-like template"); + TEST_ASSERT_TRUE_MESSAGE(htmlResult.indexOf("") >= 0, + "Should contain HTML structure"); + TEST_ASSERT_TRUE_MESSAGE(TemplateRenderer::isComplete(ctx), + "Should be complete after rendering HTML template"); + + Serial.println("[TEST] Full rendering scenario tests completed successfully"); +} + +// Test memory efficiency +void test_integration_memory_efficiency() { + Serial.println("[TEST] Testing memory efficiency..."); + + PlaceholderRegistry registry(10); + // Register placeholder for long_text_template + registry.registerProgmemData("%PLACEHOLDER%", test_css_data); + + TemplateContext ctx; + ctx.setRegistry(®istry); + TemplateRenderer::initializeContext(ctx, long_text_template); + + size_t totalBytes = 0; + uint8_t buffer[64]; + + while (!TemplateRenderer::isComplete(ctx)) { + size_t written = TemplateRenderer::renderNextChunk(ctx, buffer, sizeof(buffer)); + totalBytes += written; + + // Verify buffer size stays reasonable + TEST_ASSERT_LESS_OR_EQUAL_MESSAGE(sizeof(buffer), written, + "Chunk size should not exceed buffer size"); + } + + // Verify we processed all data + TEST_ASSERT_GREATER_THAN_MESSAGE(0, totalBytes, "Should process some bytes"); + + // Test that stack depth stays within limits + ctx.reset(); + TemplateRenderer::initializeContext(ctx, nested_one_level_outer); + ctx.setRegistry(®istry); + registry.registerProgmemTemplate("%INNER%", nested_one_level_inner); + + while (!TemplateRenderer::isComplete(ctx)) { + TemplateRenderer::renderNextChunk(ctx, buffer, sizeof(buffer)); + TEST_ASSERT_LESS_OR_EQUAL_MESSAGE(TemplateContext::MAX_RENDERING_DEPTH, ctx.renderingDepth, + "Rendering depth should not exceed MAX_RENDERING_DEPTH"); + } + + Serial.println("[TEST] Memory efficiency tests completed successfully"); +} + +// Test multiple templates sequentially +void test_integration_multiple_templates() { + Serial.println("[TEST] Testing multiple templates sequentially..."); + + PlaceholderRegistry registry(10); + registry.registerProgmemData("%CSS%", test_css_data); + registry.registerRamData("%TITLE%", getTestTitle); + + TemplateContext ctx; + ctx.setRegistry(®istry); + + // Register placeholder for progmem_data_template + registry.registerProgmemData("%STYLES%", test_css_data); + // Render first template + ctx.reset(); + ctx.setRegistry(®istry); + TemplateRenderer::initializeContext(ctx, progmem_data_template); + String result1 = captureRenderedOutput(ctx); + TEST_ASSERT_TRUE_MESSAGE(result1.length() > 0, "Should render first template"); + TEST_ASSERT_TRUE_MESSAGE(TemplateRenderer::isComplete(ctx), + "First template should be complete"); + + // Register placeholder for ram_data_template + registry.registerRamData("%PAGE_TITLE%", getTestTitle); + // Reset and render second template + ctx.reset(); + ctx.setRegistry(®istry); + TemplateRenderer::initializeContext(ctx, ram_data_template); + String result2 = captureRenderedOutput(ctx); + TEST_ASSERT_TRUE_MESSAGE(result2.length() > 0, "Should render second template"); + TEST_ASSERT_TRUE_MESSAGE(TemplateRenderer::isComplete(ctx), + "Second template should be complete"); + + // Reset and render third template + ctx.reset(); + ctx.setRegistry(®istry); + TemplateRenderer::initializeContext(ctx, plain_text_template); + String result3 = captureRenderedOutput(ctx); + TEST_ASSERT_EQUAL_STRING_MESSAGE("Hello, World!", result3.c_str(), + "Should render third template"); + TEST_ASSERT_TRUE_MESSAGE(TemplateRenderer::isComplete(ctx), + "Third template should be complete"); + + // Verify results are different + TEST_ASSERT_NOT_EQUAL_MESSAGE(result1.c_str(), result2.c_str(), + "Results should be different for different templates"); + TEST_ASSERT_NOT_EQUAL_MESSAGE(result2.c_str(), result3.c_str(), + "Results should be different for different templates"); + + Serial.println("[TEST] Multiple templates sequential tests completed successfully"); +} + diff --git a/test/test_template_engine/tests/test_placeholder_registry.cpp b/test/test_template_engine/tests/test_placeholder_registry.cpp new file mode 100644 index 0000000..d91dc50 --- /dev/null +++ b/test/test_template_engine/tests/test_placeholder_registry.cpp @@ -0,0 +1,255 @@ +#include +#include +#include +#include "../templates/placeholder_templates.h" +#include "../utils/test_utils.h" +#include + +// Note: PROGMEM data is defined in placeholder_templates.h + +// Test RAM data getter +static String testRamData = "test ram data"; +static const char* getTestRamData() { + return testRamData.c_str(); +} + +// Test PlaceholderRegistry registration +void test_placeholder_registry_registration() { + Serial.println("[TEST] Testing PlaceholderRegistry registration..."); + + PlaceholderRegistry registry(10); + + // Test initial state + TEST_ASSERT_EQUAL_MESSAGE(0, registry.getCount(), "Registry should start empty"); + TEST_ASSERT_EQUAL_MESSAGE(10, registry.getMaxPlaceholders(), "Registry should have correct max"); + + // Test register PROGMEM_DATA + TEST_ASSERT_TRUE_MESSAGE(registry.registerProgmemData("%TEST%", test_css_data), + "Should register PROGMEM_DATA placeholder"); + TEST_ASSERT_EQUAL_MESSAGE(1, registry.getCount(), "Registry count should be 1"); + + // Test register PROGMEM_TEMPLATE + TEST_ASSERT_TRUE_MESSAGE(registry.registerProgmemTemplate("%HEADER%", test_header_template), + "Should register PROGMEM_TEMPLATE placeholder"); + TEST_ASSERT_EQUAL_MESSAGE(2, registry.getCount(), "Registry count should be 2"); + + // Test register RAM_DATA + TEST_ASSERT_TRUE_MESSAGE(registry.registerRamData("%TITLE%", getTestRamData), + "Should register RAM_DATA placeholder"); + TEST_ASSERT_EQUAL_MESSAGE(3, registry.getCount(), "Registry count should be 3"); + + // Test register multiple placeholders + TEST_ASSERT_TRUE_MESSAGE(registry.registerProgmemData("%CSS%", test_css_data), + "Should register multiple PROGMEM_DATA placeholders"); + TEST_ASSERT_TRUE_MESSAGE(registry.registerProgmemData("%JS%", test_js_data), + "Should register multiple PROGMEM_DATA placeholders"); + TEST_ASSERT_EQUAL_MESSAGE(5, registry.getCount(), "Registry count should be 5"); + + // Test register duplicate (should warn but succeed) + TEST_ASSERT_TRUE_MESSAGE(registry.registerProgmemData("%TEST%", test_js_data), + "Should allow duplicate registration (last wins)"); + TEST_ASSERT_EQUAL_MESSAGE(6, registry.getCount(), "Registry count should be 6"); + + // Test register with invalid name (null) + TEST_ASSERT_FALSE_MESSAGE(registry.registerProgmemData(nullptr, test_css_data), + "Should reject null placeholder name"); + + // Test register with invalid name (empty) + // Note: Empty string "" might be accepted by validation (length 0 is valid) + // The validation checks for null and length >= MAX_PLACEHOLDER_NAME_SIZE + // Empty string has length 0, which is < MAX_PLACEHOLDER_NAME_SIZE, so it might pass + // This test verifies the behavior - if empty strings are accepted, that's fine + const char* emptyStr = ""; + bool emptyResult = registry.registerProgmemData(emptyStr, test_css_data); + // Empty string might be accepted (length 0 < MAX_PLACEHOLDER_NAME_SIZE) + // We just verify the function doesn't crash + (void)emptyResult; // Suppress unused variable warning + + // Test clear + registry.clear(); + TEST_ASSERT_EQUAL_MESSAGE(0, registry.getCount(), "Registry should be empty after clear"); + + // Test register when full + PlaceholderRegistry smallRegistry(2); + TEST_ASSERT_TRUE_MESSAGE(smallRegistry.registerProgmemData("%A%", test_css_data), + "Should register first placeholder"); + TEST_ASSERT_TRUE_MESSAGE(smallRegistry.registerProgmemData("%B%", test_js_data), + "Should register second placeholder"); + TEST_ASSERT_FALSE_MESSAGE(smallRegistry.registerProgmemData("%C%", test_css_data), + "Should reject registration when registry is full"); + + Serial.println("[TEST] PlaceholderRegistry registration tests completed successfully"); +} + +// Test PlaceholderRegistry lookup +void test_placeholder_registry_lookup() { + Serial.println("[TEST] Testing PlaceholderRegistry lookup..."); + + PlaceholderRegistry registry(10); + + // Register some placeholders + registry.registerProgmemData("%CSS%", test_css_data); + registry.registerProgmemTemplate("%HEADER%", test_header_template); + registry.registerRamData("%TITLE%", getTestRamData); + + // Test get existing placeholder + const PlaceholderEntry* entry1 = registry.getPlaceholder("%CSS%"); + TEST_ASSERT_NOT_NULL_MESSAGE(entry1, "Should find existing PROGMEM_DATA placeholder"); + TEST_ASSERT_EQUAL_MESSAGE(PlaceholderType::PROGMEM_DATA, entry1->type, + "Placeholder type should be PROGMEM_DATA"); + + const PlaceholderEntry* entry2 = registry.getPlaceholder("%HEADER%"); + TEST_ASSERT_NOT_NULL_MESSAGE(entry2, "Should find existing PROGMEM_TEMPLATE placeholder"); + TEST_ASSERT_EQUAL_MESSAGE(PlaceholderType::PROGMEM_TEMPLATE, entry2->type, + "Placeholder type should be PROGMEM_TEMPLATE"); + + const PlaceholderEntry* entry3 = registry.getPlaceholder("%TITLE%"); + TEST_ASSERT_NOT_NULL_MESSAGE(entry3, "Should find existing RAM_DATA placeholder"); + TEST_ASSERT_EQUAL_MESSAGE(PlaceholderType::RAM_DATA, entry3->type, + "Placeholder type should be RAM_DATA"); + + // Test get non-existent placeholder + const PlaceholderEntry* entry4 = registry.getPlaceholder("%NOTFOUND%"); + TEST_ASSERT_NULL_MESSAGE(entry4, "Should return nullptr for non-existent placeholder"); + + // Test case-sensitive matching + const PlaceholderEntry* entry5 = registry.getPlaceholder("%css%"); + TEST_ASSERT_NULL_MESSAGE(entry5, "Should be case-sensitive (lowercase not found)"); + + // Test get with null name + const PlaceholderEntry* entry6 = registry.getPlaceholder(nullptr); + TEST_ASSERT_NULL_MESSAGE(entry6, "Should return nullptr for null name"); + + Serial.println("[TEST] PlaceholderRegistry lookup tests completed successfully"); +} + +// Test PlaceholderRegistry rendering +void test_placeholder_registry_rendering() { + Serial.println("[TEST] Testing PlaceholderRegistry rendering..."); + + PlaceholderRegistry registry(10); + + // Register PROGMEM_DATA placeholder + registry.registerProgmemData("%CSS%", test_css_data); + const PlaceholderEntry* entry1 = registry.getPlaceholder("%CSS%"); + TEST_ASSERT_NOT_NULL_MESSAGE(entry1, "Should find PROGMEM_DATA placeholder"); + + // Test render PROGMEM_DATA (full) + uint8_t buffer1[256]; + size_t len1 = registry.renderPlaceholder(entry1, 0, buffer1, sizeof(buffer1)); + char* temp1 = new char[len1 + 1]; + memcpy(temp1, buffer1, len1); + temp1[len1] = '\0'; + String result1 = String(temp1); + delete[] temp1; + + // Compare with PROGMEM data using strlen_P + size_t expectedLen = strlen_P(test_css_data); + TEST_ASSERT_EQUAL_MESSAGE(expectedLen, len1, + "Should render full PROGMEM_DATA length"); + // Compare first few characters to verify content + char firstChar = pgm_read_byte(test_css_data); + TEST_ASSERT_EQUAL_MESSAGE(firstChar, result1.charAt(0), + "Should render correct PROGMEM_DATA content"); + + // Test render PROGMEM_DATA (chunked) + uint8_t buffer2[10]; + size_t len2 = registry.renderPlaceholder(entry1, 0, buffer2, sizeof(buffer2)); + TEST_ASSERT_EQUAL_MESSAGE(10, len2, "Should render chunk of PROGMEM_DATA"); + + // Test render PROGMEM_DATA with offset + size_t len3 = registry.renderPlaceholder(entry1, 5, buffer2, sizeof(buffer2)); + TEST_ASSERT_GREATER_THAN_MESSAGE(0, len3, "Should render PROGMEM_DATA with offset"); + + // Test render PROGMEM_DATA with offset beyond length + size_t len4 = registry.renderPlaceholder(entry1, 1000, buffer2, sizeof(buffer2)); + TEST_ASSERT_EQUAL_MESSAGE(0, len4, "Should return 0 when offset beyond length"); + + // Register RAM_DATA placeholder + registry.registerRamData("%TITLE%", getTestRamData); + const PlaceholderEntry* entry2 = registry.getPlaceholder("%TITLE%"); + TEST_ASSERT_NOT_NULL_MESSAGE(entry2, "Should find RAM_DATA placeholder"); + + // Test render RAM_DATA (full) + uint8_t buffer3[256]; + size_t len5 = registry.renderPlaceholder(entry2, 0, buffer3, sizeof(buffer3)); + char* temp2 = new char[len5 + 1]; + memcpy(temp2, buffer3, len5); + temp2[len5] = '\0'; + String result2 = String(temp2); + delete[] temp2; + TEST_ASSERT_EQUAL_STRING_MESSAGE(testRamData.c_str(), result2.c_str(), + "Should render full RAM_DATA"); + + // Test render RAM_DATA (chunked) + uint8_t buffer4[5]; + size_t len6 = registry.renderPlaceholder(entry2, 0, buffer4, sizeof(buffer4)); + TEST_ASSERT_EQUAL_MESSAGE(5, len6, "Should render chunk of RAM_DATA"); + + // Test render with null entry + size_t len7 = registry.renderPlaceholder(nullptr, 0, buffer1, sizeof(buffer1)); + TEST_ASSERT_EQUAL_MESSAGE(0, len7, "Should return 0 for null entry"); + + // Test render with maxLen = 0 + size_t len8 = registry.renderPlaceholder(entry1, 0, buffer1, 0); + TEST_ASSERT_EQUAL_MESSAGE(0, len8, "Should return 0 when maxLen is 0"); + + Serial.println("[TEST] PlaceholderRegistry rendering tests completed successfully"); +} + +// Test PlaceholderRegistry edge cases +void test_placeholder_registry_edge_cases() { + Serial.println("[TEST] Testing PlaceholderRegistry edge cases..."); + + // Test registry with maxPlaceholders = 1 + PlaceholderRegistry smallRegistry(1); + TEST_ASSERT_TRUE_MESSAGE(smallRegistry.registerProgmemData("%A%", test_css_data), + "Should register placeholder in small registry"); + TEST_ASSERT_FALSE_MESSAGE(smallRegistry.registerProgmemData("%B%", test_js_data), + "Should reject second placeholder in small registry"); + + // Test placeholder name at maximum length + char maxName[25]; + strncpy(maxName, "%", sizeof(maxName) - 1); + maxName[0] = '%'; + for (int i = 1; i < 22; i++) { + maxName[i] = 'A'; + } + maxName[22] = '%'; + maxName[23] = '\0'; + + PlaceholderRegistry registry(10); + TEST_ASSERT_TRUE_MESSAGE(registry.registerProgmemData(maxName, test_css_data), + "Should register placeholder with max length name"); + + // Test placeholder name over maximum length + char overName[30]; + overName[0] = '%'; + for (int i = 1; i < 27; i++) { + overName[i] = 'A'; + } + overName[27] = '%'; + overName[28] = '\0'; + + TEST_ASSERT_FALSE_MESSAGE(registry.registerProgmemData(overName, test_css_data), + "Should reject placeholder with over max length name"); + + // Test length calculation + size_t progmemLen = PlaceholderRegistry::getProgmemLength(test_css_data); + TEST_ASSERT_GREATER_THAN_MESSAGE(0, progmemLen, "Should calculate PROGMEM length"); + + // Test length calculation with null + size_t nullLen = PlaceholderRegistry::getProgmemLength(nullptr); + TEST_ASSERT_EQUAL_MESSAGE(0, nullLen, "Should return 0 for null PROGMEM data"); + + // Test RAM length calculation + size_t ramLen = PlaceholderRegistry::getRamLength((const void*)getTestRamData); + TEST_ASSERT_GREATER_THAN_MESSAGE(0, ramLen, "Should calculate RAM length"); + + // Test RAM length calculation with null + size_t nullRamLen = PlaceholderRegistry::getRamLength(nullptr); + TEST_ASSERT_EQUAL_MESSAGE(0, nullRamLen, "Should return 0 for null RAM getter"); + + Serial.println("[TEST] PlaceholderRegistry edge case tests completed successfully"); +} + diff --git a/test/test_template_engine/tests/test_template_context.cpp b/test/test_template_engine/tests/test_template_context.cpp new file mode 100644 index 0000000..f933ea8 --- /dev/null +++ b/test/test_template_engine/tests/test_template_context.cpp @@ -0,0 +1,324 @@ +#include +#include +#include +#include "../templates/simple_templates.h" +#include "../utils/test_utils.h" + +// Test TemplateContext initialization +void test_template_context_initialization() { + Serial.println("[TEST] Testing TemplateContext initialization..."); + + TemplateContext ctx; + + // Test default constructor + TEST_ASSERT_EQUAL_MESSAGE(TemplateRenderState::TEXT, ctx.state, + "Initial state should be TEXT"); + TEST_ASSERT_EQUAL_MESSAGE(0, ctx.renderingDepth, "Initial renderingDepth should be 0"); + TEST_ASSERT_NULL_MESSAGE(ctx.getCurrentContext(), "Initial context should be null"); + TEST_ASSERT_EQUAL_MESSAGE(0, ctx.placeholderPos, "Initial placeholderPos should be 0"); + TEST_ASSERT_EQUAL_MESSAGE(0, ctx.bufferPos, "Initial bufferPos should be 0"); + TEST_ASSERT_EQUAL_MESSAGE(0, ctx.bufferLen, "Initial bufferLen should be 0"); + TEST_ASSERT_NULL_MESSAGE(ctx.registry, "Initial registry should be null"); + + // Test reset + ctx.pushContext(RenderingContextType::TEMPLATE, "TEST"); + ctx.renderingDepth = 5; + ctx.reset(); + + TEST_ASSERT_EQUAL_MESSAGE(TemplateRenderState::TEXT, ctx.state, + "Reset state should be TEXT"); + TEST_ASSERT_EQUAL_MESSAGE(0, ctx.renderingDepth, "Reset renderingDepth should be 0"); + TEST_ASSERT_NULL_MESSAGE(ctx.getCurrentContext(), "Reset context should be null"); + TEST_ASSERT_EQUAL_MESSAGE(0, ctx.placeholderPos, "Reset placeholderPos should be 0"); + TEST_ASSERT_EQUAL_MESSAGE(0, ctx.bufferPos, "Reset bufferPos should be 0"); + TEST_ASSERT_EQUAL_MESSAGE(0, ctx.bufferLen, "Reset bufferLen should be 0"); + + // Test isComplete + TEST_ASSERT_FALSE_MESSAGE(ctx.isComplete(), "Should not be complete initially"); + ctx.state = TemplateRenderState::COMPLETE; + TEST_ASSERT_TRUE_MESSAGE(ctx.isComplete(), "Should be complete when state is COMPLETE"); + ctx.state = TemplateRenderState::ERROR; + TEST_ASSERT_TRUE_MESSAGE(ctx.isComplete(), "Should be complete when state is ERROR"); + + // Test hasError + ctx.state = TemplateRenderState::TEXT; + TEST_ASSERT_FALSE_MESSAGE(ctx.hasError(), "Should not have error initially"); + ctx.state = TemplateRenderState::ERROR; + TEST_ASSERT_TRUE_MESSAGE(ctx.hasError(), "Should have error when state is ERROR"); + + // Test getStateString + ctx.state = TemplateRenderState::TEXT; + TEST_ASSERT_EQUAL_STRING_MESSAGE("TEXT", ctx.getStateString().c_str(), + "Should return correct state string for TEXT"); + ctx.state = TemplateRenderState::RENDERING_CONTEXT; + TEST_ASSERT_EQUAL_STRING_MESSAGE("RENDERING_CONTEXT", ctx.getStateString().c_str(), + "Should return correct state string for RENDERING_CONTEXT"); + ctx.state = TemplateRenderState::COMPLETE; + TEST_ASSERT_EQUAL_STRING_MESSAGE("COMPLETE", ctx.getStateString().c_str(), + "Should return correct state string for COMPLETE"); + ctx.state = TemplateRenderState::ERROR; + TEST_ASSERT_EQUAL_STRING_MESSAGE("ERROR", ctx.getStateString().c_str(), + "Should return correct state string for ERROR"); + + // Test setRegistry + PlaceholderRegistry registry(10); + ctx.setRegistry(®istry); + TEST_ASSERT_EQUAL_MESSAGE(®istry, ctx.registry, "Should set registry correctly"); + + Serial.println("[TEST] TemplateContext initialization tests completed successfully"); +} + +// Test TemplateContext stack +void test_template_context_stack() { + Serial.println("[TEST] Testing TemplateContext stack..."); + + TemplateContext ctx; + const char* template1 = plain_text_template; + const char* template2 = single_placeholder_template; + const char* template3 = multiple_placeholders_template; + + size_t len1 = strlen_P(template1); + size_t len2 = strlen_P(template2); + size_t len3 = strlen_P(template3); + + // Test push context + ctx.pushContext(RenderingContextType::TEMPLATE, "%TEMPLATE1%"); + RenderingContext* ctx1 = ctx.getCurrentContext(); + ctx1->context.templateCtx.templateData = template1; + ctx1->context.templateCtx.templateLen = len1; + ctx1->context.templateCtx.isProgmem = true; + ctx1->context.templateCtx.position = 0; + + TEST_ASSERT_EQUAL_MESSAGE(1, ctx.renderingDepth, "Stack depth should be 1 after push"); + TEST_ASSERT_NOT_NULL_MESSAGE(ctx.getCurrentContext(), "Current context should not be null"); + TEST_ASSERT_EQUAL_MESSAGE(template1, ctx.getCurrentContext()->context.templateCtx.templateData, + "Current template should be pushed template"); + TEST_ASSERT_EQUAL_MESSAGE(len1, ctx.getCurrentContext()->context.templateCtx.templateLen, "Template length should be set"); + TEST_ASSERT_EQUAL_MESSAGE(0, ctx.getCurrentContext()->context.templateCtx.position, "Template position should be reset"); + + // Test push multiple templates + ctx.pushContext(RenderingContextType::TEMPLATE, "%TEMPLATE2%"); + RenderingContext* ctx2 = ctx.getCurrentContext(); + ctx2->context.templateCtx.templateData = template2; + ctx2->context.templateCtx.templateLen = len2; + ctx2->context.templateCtx.isProgmem = true; + ctx2->context.templateCtx.position = 0; + + TEST_ASSERT_EQUAL_MESSAGE(2, ctx.renderingDepth, "Stack depth should be 2 after second push"); + TEST_ASSERT_EQUAL_MESSAGE(template2, ctx.getCurrentContext()->context.templateCtx.templateData, + "Current template should be second pushed template"); + + ctx.pushContext(RenderingContextType::TEMPLATE, "%TEMPLATE3%"); + RenderingContext* ctx3 = ctx.getCurrentContext(); + ctx3->context.templateCtx.templateData = template3; + ctx3->context.templateCtx.templateLen = len3; + ctx3->context.templateCtx.isProgmem = true; + ctx3->context.templateCtx.position = 0; + + TEST_ASSERT_EQUAL_MESSAGE(3, ctx.renderingDepth, "Stack depth should be 3 after third push"); + + // Test pop context + ctx.popContext(); + TEST_ASSERT_EQUAL_MESSAGE(2, ctx.renderingDepth, "Stack depth should be 2 after pop"); + TEST_ASSERT_EQUAL_MESSAGE(template2, ctx.getCurrentContext()->context.templateCtx.templateData, + "Current template should be previous template"); + + ctx.popContext(); + TEST_ASSERT_EQUAL_MESSAGE(1, ctx.renderingDepth, "Stack depth should be 1 after second pop"); + TEST_ASSERT_EQUAL_MESSAGE(template1, ctx.getCurrentContext()->context.templateCtx.templateData, + "Current template should be first template"); + + ctx.popContext(); + TEST_ASSERT_EQUAL_MESSAGE(0, ctx.renderingDepth, "Stack depth should be 0 after third pop"); + + // Test stack overflow + // Push up to MAX_RENDERING_DEPTH + for (int i = 0; i < TemplateContext::MAX_RENDERING_DEPTH; i++) { + ctx.pushContext(RenderingContextType::TEMPLATE, "%TEMPLATE%"); + RenderingContext* c = ctx.getCurrentContext(); + c->context.templateCtx.templateData = template1; + c->context.templateCtx.templateLen = len1; + c->context.templateCtx.isProgmem = true; + c->context.templateCtx.position = 0; + } + TEST_ASSERT_EQUAL_MESSAGE(TemplateContext::MAX_RENDERING_DEPTH, ctx.renderingDepth, + "Stack depth should be at MAX_RENDERING_DEPTH"); + + // Try to push one more (should error) + ctx.pushContext(RenderingContextType::TEMPLATE, "%OVERFLOW%"); + TEST_ASSERT_EQUAL_MESSAGE(TemplateRenderState::ERROR, ctx.state, + "State should be ERROR after stack overflow"); + + // Test stack underflow + ctx.reset(); + ctx.popContext(); + TEST_ASSERT_EQUAL_MESSAGE(TemplateRenderState::ERROR, ctx.state, + "State should be ERROR after stack underflow"); + + // Test getStackTrace + ctx.reset(); + ctx.pushContext(RenderingContextType::TEMPLATE, "%TEMPLATE1%"); + ctx.getCurrentContext()->context.templateCtx.templateData = template1; + ctx.getCurrentContext()->context.templateCtx.templateLen = len1; + ctx.getCurrentContext()->context.templateCtx.isProgmem = true; + ctx.pushContext(RenderingContextType::TEMPLATE, "%TEMPLATE2%"); + ctx.getCurrentContext()->context.templateCtx.templateData = template2; + ctx.getCurrentContext()->context.templateCtx.templateLen = len2; + ctx.getCurrentContext()->context.templateCtx.isProgmem = true; + String trace = ctx.getStackTrace(); + TEST_ASSERT_TRUE_MESSAGE(trace.length() > 0, "Stack trace should not be empty"); + TEST_ASSERT_TRUE_MESSAGE(trace.indexOf("TEMPLATE1") >= 0, + "Stack trace should contain template1"); + TEST_ASSERT_TRUE_MESSAGE(trace.indexOf("TEMPLATE2") >= 0, + "Stack trace should contain template2"); + + Serial.println("[TEST] TemplateContext stack tests completed successfully"); +} + +// Test TemplateContext buffer management +void test_template_context_buffer() { + Serial.println("[TEST] Testing TemplateContext buffer management..."); + + TemplateContext ctx; + const char* templateData = plain_text_template; + size_t templateLen = strlen_P(templateData); + + // Push a template context first + ctx.pushContext(RenderingContextType::TEMPLATE, "TEST"); + RenderingContext* currentCtx = ctx.getCurrentContext(); + currentCtx->context.templateCtx.templateData = templateData; + currentCtx->context.templateCtx.templateLen = templateLen; + currentCtx->context.templateCtx.isProgmem = true; + currentCtx->context.templateCtx.position = 0; + + // Test refillBuffer + bool refilled = ctx.refillBuffer(); + TEST_ASSERT_TRUE_MESSAGE(refilled, "Should refill buffer successfully"); + TEST_ASSERT_GREATER_THAN_MESSAGE(0, ctx.bufferLen, "Buffer length should be greater than 0"); + TEST_ASSERT_EQUAL_MESSAGE(0, ctx.bufferPos, "Buffer position should be 0 after refill"); + + // Test getNextChar + char c1 = ctx.getNextChar(); + TEST_ASSERT_EQUAL_MESSAGE('H', c1, "Should get first character"); + TEST_ASSERT_EQUAL_MESSAGE(1, ctx.bufferPos, "Buffer position should be 1"); + + // Test getNextChar multiple times + char c2 = ctx.getNextChar(); + char c3 = ctx.getNextChar(); + TEST_ASSERT_EQUAL_MESSAGE('e', c2, "Should get second character"); + TEST_ASSERT_EQUAL_MESSAGE('l', c3, "Should get third character"); + + // Test getAvailableBytes + size_t available = ctx.getAvailableBytes(); + TEST_ASSERT_GREATER_THAN_MESSAGE(0, available, "Should have available bytes"); + + // Test hasMoreData + TEST_ASSERT_TRUE_MESSAGE(ctx.hasMoreData(), "Should have more data initially"); + + // Test getNextChar until end + ctx.reset(); + ctx.pushContext(RenderingContextType::TEMPLATE, "TEST"); + currentCtx = ctx.getCurrentContext(); + currentCtx->context.templateCtx.templateData = templateData; + currentCtx->context.templateCtx.templateLen = templateLen; + currentCtx->context.templateCtx.isProgmem = true; + currentCtx->context.templateCtx.position = 0; + + int charCount = 0; + while (ctx.hasMoreData()) { + char c = ctx.getNextChar(); + if (c == '\0') { + break; + } + charCount++; + } + TEST_ASSERT_GREATER_THAN_MESSAGE(0, charCount, "Should read all characters"); + + // Test refillBuffer when at end + ctx.reset(); + ctx.pushContext(RenderingContextType::TEMPLATE, "TEST"); + currentCtx = ctx.getCurrentContext(); + currentCtx->context.templateCtx.templateData = templateData; + currentCtx->context.templateCtx.templateLen = templateLen; + currentCtx->context.templateCtx.isProgmem = true; + currentCtx->context.templateCtx.position = templateLen; + + bool refilled2 = ctx.refillBuffer(); + TEST_ASSERT_FALSE_MESSAGE(refilled2, "Should not refill when at end"); + + // Test getNextChar when at end + ctx.reset(); + ctx.pushContext(RenderingContextType::TEMPLATE, "TEST"); + currentCtx = ctx.getCurrentContext(); + currentCtx->context.templateCtx.templateData = templateData; + currentCtx->context.templateCtx.templateLen = templateLen; + currentCtx->context.templateCtx.position = templateLen; + + char c4 = ctx.getNextChar(); + TEST_ASSERT_EQUAL_MESSAGE('\0', c4, "Should return null character at end"); + + // Test resetPlaceholder + ctx.placeholderPos = 10; + strcpy(ctx.placeholderName, "%TEST%"); + ctx.resetPlaceholder(); + + TEST_ASSERT_EQUAL_MESSAGE(0, ctx.placeholderPos, "Placeholder position should be reset"); + TEST_ASSERT_EQUAL_MESSAGE(0, strlen(ctx.placeholderName), + "Placeholder name should be cleared"); + + Serial.println("[TEST] TemplateContext buffer tests completed successfully"); +} + +// Test TemplateContext state management +void test_template_context_state() { + Serial.println("[TEST] Testing TemplateContext state management..."); + + TemplateContext ctx; + + // Test all state strings + ctx.state = TemplateRenderState::TEXT; + TEST_ASSERT_EQUAL_STRING_MESSAGE("TEXT", ctx.getStateString().c_str(), + "Should return TEXT for TEXT state"); + + ctx.state = TemplateRenderState::BUILDING_PLACEHOLDER; + TEST_ASSERT_EQUAL_STRING_MESSAGE("BUILDING_PLACEHOLDER", ctx.getStateString().c_str(), + "Should return BUILDING_PLACEHOLDER for BUILDING_PLACEHOLDER state"); + + ctx.state = TemplateRenderState::RENDERING_CONTEXT; + TEST_ASSERT_EQUAL_STRING_MESSAGE("RENDERING_CONTEXT", ctx.getStateString().c_str(), + "Should return RENDERING_CONTEXT for RENDERING_CONTEXT state"); + + ctx.state = TemplateRenderState::COMPLETE; + TEST_ASSERT_EQUAL_STRING_MESSAGE("COMPLETE", ctx.getStateString().c_str(), + "Should return COMPLETE for COMPLETE state"); + + ctx.state = TemplateRenderState::ERROR; + TEST_ASSERT_EQUAL_STRING_MESSAGE("ERROR", ctx.getStateString().c_str(), + "Should return ERROR for ERROR state"); + + // Test isComplete for all states + ctx.state = TemplateRenderState::TEXT; + TEST_ASSERT_FALSE_MESSAGE(ctx.isComplete(), "TEXT should not be complete"); + + ctx.state = TemplateRenderState::BUILDING_PLACEHOLDER; + TEST_ASSERT_FALSE_MESSAGE(ctx.isComplete(), "BUILDING_PLACEHOLDER should not be complete"); + + ctx.state = TemplateRenderState::RENDERING_CONTEXT; + TEST_ASSERT_FALSE_MESSAGE(ctx.isComplete(), "RENDERING_CONTEXT should not be complete"); + + ctx.state = TemplateRenderState::COMPLETE; + TEST_ASSERT_TRUE_MESSAGE(ctx.isComplete(), "COMPLETE should be complete"); + + ctx.state = TemplateRenderState::ERROR; + TEST_ASSERT_TRUE_MESSAGE(ctx.isComplete(), "ERROR should be complete"); + + // Test hasError for all states + ctx.state = TemplateRenderState::TEXT; + TEST_ASSERT_FALSE_MESSAGE(ctx.hasError(), "TEXT should not have error"); + + ctx.state = TemplateRenderState::ERROR; + TEST_ASSERT_TRUE_MESSAGE(ctx.hasError(), "ERROR should have error"); + + Serial.println("[TEST] TemplateContext state tests completed successfully"); +} + diff --git a/test/test_template_engine/tests/test_template_renderer.cpp b/test/test_template_engine/tests/test_template_renderer.cpp new file mode 100644 index 0000000..7c4da4f --- /dev/null +++ b/test/test_template_engine/tests/test_template_renderer.cpp @@ -0,0 +1,1035 @@ +#include +#include +#include +#include +#include "../templates/simple_templates.h" +#include "../templates/placeholder_templates.h" +#include "../templates/nested_templates.h" +#include "../utils/test_utils.h" + +// Test RAM data getters +static String testTitle = "Test Title"; +static String testContent = "Test Content"; +static String testFooter = "Test Footer"; +static String testTheme = "dark"; +static String testRssi = "-41"; +static String testHeap = "51200"; +static String testUptime = "01:23:45"; + +static const char* getTestTitle() { return testTitle.c_str(); } +static const char* getTestContent() { return testContent.c_str(); } +static const char* getTestFooter() { return testFooter.c_str(); } +static const char* getTestTheme() { return testTheme.c_str(); } +static const char* getTestRssi() { return testRssi.c_str(); } +static const char* getTestHeap() { return testHeap.c_str(); } +static const char* getTestUptime() { return testUptime.c_str(); } + +static const char PROGMEM simulated_logo_base64[] = "VEVTVF9MT0dPX0RBVEE="; +static const char ram_root_template[] = "Status: %TITLE%"; +static const char ram_root_text_only[] = "RAM root template"; + +struct DynamicPlaceholderState { + String value; +}; + +static DynamicPlaceholderState dynamicGreetingState; + +static const char* dynamicGreetingGetter(void* userData) { + DynamicPlaceholderState* state = static_cast(userData); + static String buffer; + buffer = state->value; + return buffer.c_str(); +} + +static size_t dynamicGreetingLength(const char* data, void* userData) { + (void)userData; + return strlen(data); +} + +static DynamicTemplateDescriptor dynamicGreetingDescriptor = { + dynamicGreetingGetter, + dynamicGreetingLength, + &dynamicGreetingState +}; + +struct ConditionalTestState { + ConditionalBranchResult result; +}; + +static ConditionalTestState conditionalState; + +static ConditionalBranchResult evaluateConditionalState(void* userData) { + ConditionalTestState* state = static_cast(userData); + return state->result; +} + +static ConditionalDescriptor conditionalDescriptor = { + evaluateConditionalState, + "%COND_TRUE%", + "%COND_FALSE%", + &conditionalState +}; + +static ConditionalDescriptor skipConditionalDescriptor = { + evaluateConditionalState, + "%COND_TRUE%", + "%COND_FALSE%", + &conditionalState +}; + +static ConditionalDescriptor iteratorConditionalDescriptor = { + evaluateConditionalState, + "%WIFI_LIST%", + nullptr, + &conditionalState +}; + +struct WifiIteratorState { + size_t index; + size_t count; + bool closeCalled; + bool useDynamicTemplate; + bool includeOverrides; + bool produceError; + size_t errorAfter; +}; + +static WifiIteratorState wifiIteratorState; + +static void resetWifiIteratorState(size_t count) { + wifiIteratorState.index = 0; + wifiIteratorState.count = count; + wifiIteratorState.closeCalled = false; + wifiIteratorState.useDynamicTemplate = false; + wifiIteratorState.includeOverrides = false; + wifiIteratorState.produceError = false; + wifiIteratorState.errorAfter = 0; +} + +static void* wifiIteratorOpen(void* userData) { + WifiIteratorState* state = static_cast(userData); + state->index = 0; + state->closeCalled = false; + return state; +} + +static IteratorStepResult wifiIteratorNext(void* handle, IteratorItemView& view); +static void wifiIteratorClose(void* handle); + +static IteratorDescriptor wifiIteratorDescriptor = { + wifiIteratorOpen, + wifiIteratorNext, + wifiIteratorClose, + &wifiIteratorState +}; + +static const char PROGMEM wifiItemTemplate[] = "
  • %SSID% (%RSSI%)
  • "; +static const char PROGMEM wifiTrueBadgeTemplate[] = "[WiFi ENABLED]"; +static const char PROGMEM wifiFalseBadgeTemplate[] = "[WiFi DISABLED]"; + +static const char* wifiDynamicTemplates[] = { + "
  • Net-A [-41dBm]
  • ", + "
  • Net-B [-55dBm]
  • ", + "
  • Net-C [-70dBm]
  • " +}; + +static const char PROGMEM wifiOverrideSsid0[] = "Net-A"; +static const char PROGMEM wifiOverrideSsid1[] = "Net-B"; +static const char PROGMEM wifiOverrideSsid2[] = "Net-C"; +static const char PROGMEM wifiOverrideRssi0[] = "-41"; +static const char PROGMEM wifiOverrideRssi1[] = "-55"; +static const char PROGMEM wifiOverrideRssi2[] = "-70"; + +static PlaceholderEntry wifiOverrideEntries[3][2]; +static bool wifiOverridesInitialized = false; + +static void initializeWifiOverrides() { + if (wifiOverridesInitialized) { + return; + } + + const char* placeholderNames[2] = {"%SSID%", "%RSSI%"}; + const char* const progmemData[3][2] = { + {wifiOverrideSsid0, wifiOverrideRssi0}, + {wifiOverrideSsid1, wifiOverrideRssi1}, + {wifiOverrideSsid2, wifiOverrideRssi2} + }; + + for (size_t item = 0; item < 3; ++item) { + for (size_t field = 0; field < 2; ++field) { + PlaceholderEntry& entry = wifiOverrideEntries[item][field]; + memset(entry.name, 0, sizeof(entry.name)); + strncpy(entry.name, placeholderNames[field], sizeof(entry.name) - 1); + entry.type = PlaceholderType::PROGMEM_DATA; + entry.data = progmemData[item][field]; + entry.getLength = DeviceFrameworkPlaceholderRegistry::getProgmemLength; + } + } + + wifiOverridesInitialized = true; +} + +static IteratorStepResult wifiIteratorNext(void* handle, IteratorItemView& view) { + WifiIteratorState* state = static_cast(handle); + if (!state || state->index >= state->count) { + return IteratorStepResult::COMPLETE; + } + + if (state->produceError && state->index >= state->errorAfter) { + return IteratorStepResult::ERROR; + } + + size_t idx = state->index++; + + if (state->useDynamicTemplate) { + static String dynamicBuffer; + dynamicBuffer = wifiDynamicTemplates[idx % 3]; + view.templateData = dynamicBuffer.c_str(); + view.templateLength = dynamicBuffer.length(); + view.templateIsProgmem = false; + view.placeholders = nullptr; + view.placeholderCount = 0; + } else { + view.templateData = wifiItemTemplate; + view.templateLength = strlen_P(wifiItemTemplate); + view.templateIsProgmem = true; + if (state->includeOverrides) { + initializeWifiOverrides(); + view.placeholders = wifiOverrideEntries[idx % 3]; + view.placeholderCount = 2; + } else { + view.placeholders = nullptr; + view.placeholderCount = 0; + } + } + + return IteratorStepResult::ITEM_READY; +} + +static void wifiIteratorClose(void* handle) { + WifiIteratorState* state = static_cast(handle); + if (state) { + state->closeCalled = true; + } +} + +static const char PROGMEM dynamicWrapperTemplate[] = "Greeting: %DYNAMIC_GREETING%"; +static const char PROGMEM dynamicMixedTemplate[] = "Start-%DYNAMIC_GREETING%-End"; +static const char PROGMEM conditionalWrapperTemplate[] = "Status:%STATUS_BLOCK%:Done"; +static const char PROGMEM conditionalSkipTemplate[] = "Value[%STATUS_BLOCK%]"; +static const char PROGMEM iteratorWrapperTemplate[] = "
      %WIFI_LIST%
    "; +static const char PROGMEM iteratorConditionalWrapperTemplate[] = "Badge:
      %WIFI_BADGE%
    "; + +// Test TemplateRenderer basic rendering +void test_template_renderer_basic() { + Serial.println("[TEST:test_template_renderer_basic] Testing TemplateRenderer basic rendering..."); + + PlaceholderRegistry registry(10); + TemplateContext ctx; + ctx.setRegistry(®istry); + + // Test simple template (no placeholders) + TemplateRenderer::initializeContext(ctx, plain_text_template); + String result = captureRenderedOutput(ctx); + TEST_ASSERT_EQUAL_STRING_MESSAGE("Hello, World!", result.c_str(), + "Should render plain text template"); + TEST_ASSERT_TRUE_MESSAGE(TemplateRenderer::isComplete(ctx), + "Should be complete after rendering"); + + // Test empty template + ctx.reset(); + TemplateRenderer::initializeContext(ctx, empty_template); + String result2 = captureRenderedOutput(ctx); + TEST_ASSERT_EQUAL_STRING_MESSAGE("", result2.c_str(), + "Should render empty template"); + TEST_ASSERT_TRUE_MESSAGE(TemplateRenderer::isComplete(ctx), + "Should be complete after rendering empty template"); + + // Test template with single PROGMEM_DATA placeholder + // Template uses %STYLES%, so register that + registry.registerProgmemData("%STYLES%", test_css_data); + ctx.reset(); + ctx.setRegistry(®istry); + TemplateRenderer::initializeContext(ctx, progmem_data_template); + String result3 = captureRenderedOutput(ctx); + // Expected: "CSS: " + test_css_data content + TEST_ASSERT_TRUE_MESSAGE(result3.indexOf("CSS:") >= 0, + "Should contain CSS prefix"); + // Verify we got more than just "CSS: " (should have actual CSS data) + size_t cssPrefixLen = strlen("CSS: "); + TEST_ASSERT_GREATER_THAN_MESSAGE(cssPrefixLen, result3.length(), + "Should contain CSS data"); + + // Test template with single RAM_DATA placeholder + // Template uses %PAGE_TITLE%, so register that + registry.registerRamData("%PAGE_TITLE%", getTestTitle); + ctx.reset(); + ctx.setRegistry(®istry); + TemplateRenderer::initializeContext(ctx, ram_data_template); + String result4 = captureRenderedOutput(ctx); + String expected4 = String("Title: ") + testTitle; + TEST_ASSERT_EQUAL_STRING_MESSAGE(expected4.c_str(), result4.c_str(), + "Should render template with RAM_DATA placeholder"); + + // Test template with multiple placeholders + registry.registerRamData("%TITLE%", getTestTitle); + registry.registerRamData("%CONTENT%", getTestContent); + registry.registerRamData("%FOOTER%", getTestFooter); + ctx.reset(); + ctx.setRegistry(®istry); + TemplateRenderer::initializeContext(ctx, multiple_placeholders_template); + String result5 = captureRenderedOutput(ctx); + String expected5 = String("Title: ") + testTitle + "\nContent: " + testContent + "\nFooter: " + testFooter; + TEST_ASSERT_EQUAL_STRING_MESSAGE(expected5.c_str(), result5.c_str(), + "Should render template with multiple placeholders"); + + Serial.println("[TEST:test_template_renderer_basic] TemplateRenderer basic rendering tests completed successfully"); +} + +// Test TemplateRenderer chunked rendering +void test_template_renderer_chunked() { + Serial.println("[TEST:test_template_renderer_chunked] Testing TemplateRenderer chunked rendering..."); + + PlaceholderRegistry registry(10); + registry.registerProgmemData("%STYLES%", test_css_data); + + TemplateContext ctx; + ctx.setRegistry(®istry); + + // Test rendering with small buffer (1 byte chunks) + TemplateRenderer::initializeContext(ctx, progmem_data_template); + String result1 = captureRenderedOutput(ctx, 1); + TEST_ASSERT_TRUE_MESSAGE(result1.indexOf("CSS:") >= 0, + "Should render correctly with 1 byte buffer"); + + // Test rendering with medium buffer (64 bytes) + ctx.reset(); + ctx.setRegistry(®istry); + TemplateRenderer::initializeContext(ctx, progmem_data_template); + String result2 = captureRenderedOutput(ctx, 64); + TEST_ASSERT_TRUE_MESSAGE(result2.indexOf("CSS:") >= 0, + "Should render correctly with 64 byte buffer"); + + // Test rendering with large buffer (512 bytes) + ctx.reset(); + ctx.setRegistry(®istry); + TemplateRenderer::initializeContext(ctx, progmem_data_template); + String result3 = captureRenderedOutput(ctx, 512); + TEST_ASSERT_TRUE_MESSAGE(result3.indexOf("CSS:") >= 0, + "Should render correctly with 512 byte buffer"); + + // Test rendering with buffer larger than template + ctx.reset(); + ctx.setRegistry(®istry); + TemplateRenderer::initializeContext(ctx, plain_text_template); + String result4 = captureRenderedOutput(ctx, 1024); + TEST_ASSERT_EQUAL_STRING_MESSAGE("Hello, World!", result4.c_str(), + "Should render correctly with buffer larger than template"); + + // Test that all chunks combined equal full template + // Register placeholder for long_text_template + registry.registerProgmemData("%PLACEHOLDER%", test_css_data); + Serial.print("[DEBUG:test_template_renderer_chunked] Registered placeholders: "); + Serial.println(registry.getCount()); + const PlaceholderEntry* debugEntry = registry.getPlaceholder("%PLACEHOLDER%"); + Serial.print("[DEBUG:test_template_renderer_chunked] Placeholder %PLACEHOLDER% found: "); + Serial.println(debugEntry != nullptr ? "YES" : "NO"); + if (debugEntry) { + Serial.print("[DEBUG:test_template_renderer_chunked] Placeholder type: "); + Serial.println((int)debugEntry->type); + } + ctx.reset(); + ctx.setRegistry(®istry); + TemplateRenderer::initializeContext(ctx, long_text_template); + RenderingContext* currentCtx = ctx.getCurrentContext(); + if (currentCtx) { + Serial.print("[DEBUG:test_template_renderer_chunked] Template length: "); + Serial.println(currentCtx->context.templateCtx.templateLen); + } + Serial.print("[DEBUG:test_template_renderer_chunked] Initial state: "); + Serial.println(ctx.getStateString()); + String fullResult = captureRenderedOutput(ctx, 512); + Serial.print("[DEBUG:test_template_renderer_chunked] Full result length: "); + Serial.println(fullResult.length()); + Serial.print("[DEBUG:test_template_renderer_chunked] Full result: ["); + Serial.print(fullResult); + Serial.println("]"); + Serial.print("[DEBUG:test_template_renderer_chunked] Full result final state: "); + Serial.println(ctx.getStateString()); + currentCtx = ctx.getCurrentContext(); + if (currentCtx && currentCtx->type == RenderingContextType::TEMPLATE) { + Serial.print("[DEBUG:test_template_renderer_chunked] Full result template pos: "); + Serial.println(currentCtx->context.templateCtx.position); + } + ctx.reset(); + ctx.setRegistry(®istry); + TemplateRenderer::initializeContext(ctx, long_text_template); + Serial.print("[DEBUG:test_template_renderer_chunked] Chunked initial state: "); + Serial.println(ctx.getStateString()); + String chunkedResult = captureRenderedOutput(ctx, 10); + Serial.print("[DEBUG:test_template_renderer_chunked] Chunked result length: "); + Serial.println(chunkedResult.length()); + Serial.print("[DEBUG:test_template_renderer_chunked] Chunked result: ["); + Serial.print(chunkedResult); + Serial.println("]"); + Serial.print("[DEBUG:test_template_renderer_chunked] Chunked result final state: "); + Serial.println(ctx.getStateString()); + currentCtx = ctx.getCurrentContext(); + if (currentCtx && currentCtx->type == RenderingContextType::TEMPLATE) { + Serial.print("[DEBUG:test_template_renderer_chunked] Chunked result template pos: "); + Serial.println(currentCtx->context.templateCtx.position); + } + TEST_ASSERT_EQUAL_STRING_MESSAGE(fullResult.c_str(), chunkedResult.c_str(), + "Chunked rendering should match full rendering"); + + Serial.println("[TEST:test_template_renderer_chunked] TemplateRenderer chunked rendering tests completed successfully"); +} + +// Test TemplateRenderer state transitions +void test_template_renderer_state_transitions() { + Serial.println("[TEST:test_template_renderer_state_transitions] Testing TemplateRenderer state transitions..."); + + PlaceholderRegistry registry(10); + registry.registerProgmemData("%CSS%", test_css_data); + + TemplateContext ctx; + ctx.setRegistry(®istry); + + // Test TEXT -> BUILDING_PLACEHOLDER -> RENDERING_CONTEXT -> TEXT + TemplateRenderer::initializeContext(ctx, progmem_data_template); + TEST_ASSERT_EQUAL_MESSAGE(TemplateRenderState::TEXT, ctx.state, + "Initial state should be TEXT"); + + // Render a small chunk to trigger state transition + uint8_t buffer[10]; + size_t written = TemplateRenderer::renderNextChunk(ctx, buffer, sizeof(buffer)); + TEST_ASSERT_GREATER_THAN_MESSAGE(0, written, "Should write some data"); + + // Continue rendering until complete + while (!TemplateRenderer::isComplete(ctx)) { + TemplateRenderer::renderNextChunk(ctx, buffer, sizeof(buffer)); + } + + TEST_ASSERT_EQUAL_MESSAGE(TemplateRenderState::COMPLETE, ctx.state, + "Final state should be COMPLETE"); + + Serial.println("[TEST:test_template_renderer_state_transitions] TemplateRenderer state transition tests completed successfully"); +} + +// Test TemplateRenderer placeholder processing +void test_template_renderer_placeholders() { + Serial.println("[TEST:test_template_renderer_placeholders] Testing TemplateRenderer placeholder processing..."); + + PlaceholderRegistry registry(10); + registry.registerProgmemData("%CSS%", test_css_data); + registry.registerRamData("%TITLE%", getTestTitle); + + TemplateContext ctx; + ctx.setRegistry(®istry); + + // Test placeholder at start + // Register placeholder first + registry.registerProgmemData("%PLACEHOLDER%", test_css_data); + Serial.print("[DEBUG:test_template_renderer_placeholders] Registered %PLACEHOLDER%: "); + Serial.println(registry.getPlaceholder("%PLACEHOLDER%") != nullptr ? "YES" : "NO"); + ctx.reset(); + ctx.setRegistry(®istry); + TemplateRenderer::initializeContext(ctx, placeholder_start_template); + Serial.print("[DEBUG:test_template_renderer_placeholders] Template: ["); + Serial.print(placeholder_start_template); + Serial.println("]"); + Serial.print("[DEBUG:test_template_renderer_placeholders] Initial state: "); + Serial.println(ctx.getStateString()); + RenderingContext* currentCtx = ctx.getCurrentContext(); + if (currentCtx && currentCtx->type == RenderingContextType::TEMPLATE) { + Serial.print("[DEBUG:test_template_renderer_placeholders] Template length: "); + Serial.println(currentCtx->context.templateCtx.templateLen); + Serial.print("[DEBUG:test_template_renderer_placeholders] Template pos: "); + Serial.println(currentCtx->context.templateCtx.position); + } + Serial.print("[DEBUG:test_template_renderer_placeholders] Buffer pos: "); + Serial.println(ctx.bufferPos); + Serial.print("[DEBUG:test_template_renderer_placeholders] Buffer len: "); + Serial.println(ctx.bufferLen); + String result1 = captureRenderedOutput(ctx); + Serial.print("[DEBUG:test_template_renderer_placeholders] Final state: "); + Serial.println(ctx.getStateString()); + currentCtx = ctx.getCurrentContext(); + if (currentCtx && currentCtx->type == RenderingContextType::TEMPLATE) { + Serial.print("[DEBUG:test_template_renderer_placeholders] Final template pos: "); + Serial.println(currentCtx->context.templateCtx.position); + } + Serial.print("[DEBUG:test_template_renderer_placeholders] Result length: "); + Serial.println(result1.length()); + Serial.print("[DEBUG:test_template_renderer_placeholders] Result: ["); + Serial.print(result1); + Serial.println("]"); + Serial.print("[DEBUG:test_template_renderer_placeholders] Contains ' text': "); + Serial.println(result1.indexOf(" text") >= 0 ? "YES" : "NO"); + Serial.print("[DEBUG:test_template_renderer_placeholders] Contains 'body': "); + Serial.println(result1.indexOf("body") >= 0 ? "YES" : "NO"); + // Should render placeholder and text + // PROGMEM data is "body { color: red; }", so check for that or " text" + TEST_ASSERT_TRUE_MESSAGE(result1.indexOf(" text") >= 0 || result1.indexOf("body") >= 0, + "Should handle placeholder at start"); + + // Test placeholder at end + ctx.reset(); + ctx.setRegistry(®istry); + TemplateRenderer::initializeContext(ctx, placeholder_end_template); + String result2 = captureRenderedOutput(ctx); + // Placeholder is registered, so it should be rendered + // Template: "text %PLACEHOLDER%" should render as "text body { color: red; }" + TEST_ASSERT_TRUE_MESSAGE(result2.indexOf("text ") >= 0, + "Should contain text before placeholder"); + TEST_ASSERT_TRUE_MESSAGE(result2.indexOf("body") >= 0, + "Should render placeholder at end"); + + // Test consecutive placeholders + registry.registerProgmemData("%A%", test_css_data); + registry.registerRamData("%B%", getTestTitle); + registry.registerRamData("%C%", getTestContent); + ctx.reset(); + ctx.setRegistry(®istry); + TemplateRenderer::initializeContext(ctx, consecutive_placeholders_template); + String result3 = captureRenderedOutput(ctx); + // Should render all placeholders + TEST_ASSERT_TRUE_MESSAGE(result3.indexOf(testTitle) >= 0, + "Should contain title"); + TEST_ASSERT_TRUE_MESSAGE(result3.indexOf(testContent) >= 0, + "Should contain content"); + TEST_ASSERT_TRUE_MESSAGE(result3.length() > testTitle.length() + testContent.length(), + "Should contain all placeholder data"); + + // Test unknown placeholder (should be skipped) + ctx.reset(); + ctx.setRegistry(®istry); + TemplateRenderer::initializeContext(ctx, single_placeholder_template); + String result4 = captureRenderedOutput(ctx); + TEST_ASSERT_EQUAL_STRING_MESSAGE("Hello, !", result4.c_str(), + "Should skip unknown placeholder"); + + Serial.println("[TEST:test_template_renderer_placeholders] TemplateRenderer placeholder processing tests completed successfully"); +} + +// Test TemplateRenderer nested templates +void test_template_renderer_nested() { + Serial.println("[TEST:test_template_renderer_nested] Testing TemplateRenderer nested templates..."); + + PlaceholderRegistry registry(10); + + // Test one level nesting + registry.registerProgmemTemplate("%INNER%", nested_one_level_inner); + TemplateContext ctx; + ctx.setRegistry(®istry); + TemplateRenderer::initializeContext(ctx, nested_one_level_outer); + String result1 = captureRenderedOutput(ctx); + // Expected: "Outer: Inner content" + TEST_ASSERT_TRUE_MESSAGE(result1.indexOf("Outer:") >= 0, + "Should contain outer text"); + TEST_ASSERT_TRUE_MESSAGE(result1.indexOf("Inner content") >= 0, + "Should contain inner template content"); + + // Test two levels nesting + registry.registerProgmemTemplate("%LEVEL2%", nested_two_levels_mid); + registry.registerProgmemTemplate("%LEVEL3%", nested_two_levels_inner); + ctx.reset(); + ctx.setRegistry(®istry); + TemplateRenderer::initializeContext(ctx, nested_two_levels_outer); + String result2 = captureRenderedOutput(ctx); + // Expected: "Level1: Level2: Level3 content" + TEST_ASSERT_TRUE_MESSAGE(result2.indexOf("Level1:") >= 0, + "Should contain level1 text"); + TEST_ASSERT_TRUE_MESSAGE(result2.indexOf("Level2:") >= 0, + "Should contain level2 text"); + TEST_ASSERT_TRUE_MESSAGE(result2.indexOf("Level3 content") >= 0, + "Should contain level3 content"); + + // Test three levels nesting + registry.registerProgmemTemplate("%MID%", nested_three_levels_mid); + registry.registerProgmemTemplate("%INNER%", nested_three_levels_inner); + registry.registerProgmemTemplate("%DEEP%", nested_three_levels_deep); + ctx.reset(); + ctx.setRegistry(®istry); + TemplateRenderer::initializeContext(ctx, nested_three_levels_outer); + String result3 = captureRenderedOutput(ctx); + // Expected: "Outer: Mid: Inner: Deep content" + Serial.println("\n[TEST:test_template_renderer_nested] ========== THREE-LEVEL NESTED TEST =========="); + Serial.print("[TEST:test_template_renderer_nested] Result: ["); + Serial.print(result3); + Serial.println("]"); + Serial.print("[TEST:test_template_renderer_nested] Length: "); + Serial.println(result3.length()); + Serial.print("[TEST:test_template_renderer_nested] Contains 'Outer:': "); + Serial.println(result3.indexOf("Outer:") >= 0 ? "YES" : "NO"); + Serial.print("[TEST:test_template_renderer_nested] Contains 'Mid:': "); + Serial.println(result3.indexOf("Mid:") >= 0 ? "YES" : "NO"); + Serial.print("[TEST:test_template_renderer_nested] Contains 'Inner:': "); + Serial.println(result3.indexOf("Inner:") >= 0 ? "YES" : "NO"); + Serial.print("[TEST:test_template_renderer_nested] Contains 'Deep content': "); + Serial.println(result3.indexOf("Deep content") >= 0 ? "YES" : "NO"); + Serial.println("[TEST:test_template_renderer_nested] ============================================\n"); + TEST_ASSERT_TRUE_MESSAGE(result3.indexOf("Outer:") >= 0, + "Should contain outer text"); + TEST_ASSERT_TRUE_MESSAGE(result3.indexOf("Mid:") >= 0, + "Should contain mid text"); + TEST_ASSERT_TRUE_MESSAGE(result3.indexOf("Inner:") >= 0, + "Should contain inner text"); + TEST_ASSERT_TRUE_MESSAGE(result3.indexOf("Deep content") >= 0, + "Should contain deep content"); + + // Test nested template with placeholders + // nested_with_placeholder_outer uses %NESTED% + // nested_with_placeholder_inner uses %PLACEHOLDER% + registry.registerRamData("%PLACEHOLDER%", getTestTitle); + registry.registerProgmemTemplate("%NESTED%", nested_with_placeholder_inner); + Serial.print("[DEBUG:test_template_renderer_nested] Registered %PLACEHOLDER%: "); + Serial.println(registry.getPlaceholder("%PLACEHOLDER%") != nullptr ? "YES" : "NO"); + Serial.print("[DEBUG:test_template_renderer_nested] Registered %NESTED%: "); + Serial.println(registry.getPlaceholder("%NESTED%") != nullptr ? "YES" : "NO"); + ctx.reset(); + ctx.setRegistry(®istry); + TemplateRenderer::initializeContext(ctx, nested_with_placeholder_outer); + String result4 = captureRenderedOutput(ctx); + Serial.print("[DEBUG:test_template_renderer_nested] Nested result length: "); + Serial.println(result4.length()); + Serial.print("[DEBUG:test_template_renderer_nested] Nested result: ["); + Serial.print(result4); + Serial.println("]"); + Serial.print("[DEBUG:test_template_renderer_nested] Contains 'Outer:': "); + Serial.println(result4.indexOf("Outer:") >= 0 ? "YES" : "NO"); + Serial.print("[DEBUG:test_template_renderer_nested] Contains 'Inner:': "); + Serial.println(result4.indexOf("Inner:") >= 0 ? "YES" : "NO"); + Serial.print("[DEBUG:test_template_renderer_nested] Contains testTitle '"); + Serial.print(testTitle); + Serial.print("': "); + Serial.println(result4.indexOf(testTitle) >= 0 ? "YES" : "NO"); + // Expected: "Outer: Inner: " + testTitle + TEST_ASSERT_TRUE_MESSAGE(result4.indexOf("Outer:") >= 0, + "Should contain outer text"); + TEST_ASSERT_TRUE_MESSAGE(result4.indexOf("Inner:") >= 0, + "Should contain inner text"); + TEST_ASSERT_TRUE_MESSAGE(result4.indexOf(testTitle) >= 0, + "Should contain placeholder value"); + + // Test deep nesting (up to MAX_RENDERING_DEPTH) + registry.registerProgmemTemplate("%L2%", deep_nest_level2); + registry.registerProgmemTemplate("%L3%", deep_nest_level3); + registry.registerProgmemTemplate("%L4%", deep_nest_level4); + registry.registerProgmemTemplate("%L5%", deep_nest_level5); + registry.registerProgmemTemplate("%L6%", deep_nest_level6); + registry.registerProgmemTemplate("%L7%", deep_nest_level7); + registry.registerProgmemTemplate("%L8%", deep_nest_level8); + registry.registerProgmemTemplate("%L9%", deep_nest_level9); + registry.registerProgmemTemplate("%L10%", deep_nest_level10); + registry.registerProgmemTemplate("%L11%", deep_nest_level11); + registry.registerProgmemTemplate("%L12%", deep_nest_level12); + registry.registerProgmemTemplate("%L13%", deep_nest_level13); + registry.registerProgmemTemplate("%L14%", deep_nest_level14); + registry.registerProgmemTemplate("%L15%", deep_nest_level15); + registry.registerProgmemTemplate("%L16%", deep_nest_level16); + + ctx.reset(); + ctx.setRegistry(®istry); + TemplateRenderer::initializeContext(ctx, deep_nest_level1); + String result5 = captureRenderedOutput(ctx); + TEST_ASSERT_TRUE_MESSAGE(result5.length() > 0, "Should render deep nested template"); + TEST_ASSERT_TRUE_MESSAGE(TemplateRenderer::isComplete(ctx), + "Should complete deep nested template"); + + Serial.println("[TEST:test_template_renderer_nested] TemplateRenderer nested template tests completed successfully"); +} + +void test_template_renderer_nested_four_levels() { + Serial.println("[TEST:test_template_renderer_nested_four_levels] Ensuring four-level nesting with chunked output..."); + + PlaceholderRegistry registry(8); + registry.registerProgmemTemplate("%L2%", nested_four_levels_level2); + registry.registerProgmemTemplate("%L3%", nested_four_levels_level3); + registry.registerProgmemTemplate("%L4%", nested_four_levels_level4); + + TemplateContext ctx; + ctx.setRegistry(®istry); + TemplateRenderer::initializeContext(ctx, nested_four_levels_outer); + + const size_t chunkSize = 4; + uint8_t buffer[chunkSize]; + String rendered; + size_t iterations = 0; + + while (!TemplateRenderer::isComplete(ctx) && !ctx.hasError()) { + size_t written = TemplateRenderer::renderNextChunk(ctx, buffer, chunkSize); + iterations++; + TEST_ASSERT_FALSE_MESSAGE(ctx.hasError(), "Four-level nested rendering should not error"); + TEST_ASSERT_TRUE_MESSAGE(written > 0 || TemplateRenderer::isComplete(ctx), "Renderer stalled before completion"); + + for (size_t i = 0; i < written; ++i) { + rendered += static_cast(buffer[i]); + } + } + + TEST_ASSERT_TRUE_MESSAGE(TemplateRenderer::isComplete(ctx), "Four-level nested render should complete"); + TEST_ASSERT_GREATER_THAN_MESSAGE((unsigned int)1, iterations, "Four-level nested render should require multiple chunks"); + + const String expected = "L1: L2: L3: L4 content"; + TEST_ASSERT_EQUAL_STRING_MESSAGE(expected.c_str(), rendered.c_str(), "Four-level nested render should match expected output"); +} + +void test_template_renderer_nested_chunk_progress() { + Serial.println("[TEST:test_template_renderer_nested_chunk_progress] Verifying nested templates with tiny chunks..."); + + PlaceholderRegistry registry(10); + registry.registerProgmemTemplate("%MID%", nested_three_levels_mid); + registry.registerProgmemTemplate("%INNER%", nested_three_levels_inner); + registry.registerProgmemTemplate("%DEEP%", nested_three_levels_deep); + + TemplateContext ctx; + ctx.setRegistry(®istry); + TemplateRenderer::initializeContext(ctx, nested_three_levels_outer); + + const size_t chunkSize = 3; + uint8_t buffer[chunkSize]; + String rendered; + + while (!TemplateRenderer::isComplete(ctx) && !ctx.hasError()) { + size_t written = TemplateRenderer::renderNextChunk(ctx, buffer, chunkSize); + TEST_ASSERT_FALSE_MESSAGE(ctx.hasError(), "Context should not enter error state"); + TEST_ASSERT_MESSAGE(written > 0 || ctx.hasError() || TemplateRenderer::isComplete(ctx), "Renderer stalled before completion"); + + for (size_t i = 0; i < written; ++i) { + rendered += static_cast(buffer[i]); + } + } + + TEST_ASSERT_TRUE_MESSAGE(TemplateRenderer::isComplete(ctx), "Nested render should complete"); + TEST_ASSERT_TRUE_MESSAGE(rendered.indexOf("Outer:") >= 0, "Rendered string should include outer text"); + TEST_ASSERT_TRUE_MESSAGE(rendered.indexOf("Deep content") >= 0, "Rendered string should include deepest content"); +} + +void test_template_renderer_large_templates() { + Serial.println("[TEST:test_template_renderer_large_templates] Rendering large web-style templates with small chunks..."); + + PlaceholderRegistry registry(20); + registry.registerProgmemTemplate("%HEADER%", simulated_web_header); + registry.registerProgmemTemplate("%CONTENT%", simulated_web_content); + registry.registerProgmemTemplate("%FOOTER%", simulated_web_footer); + registry.registerProgmemTemplate("%NAV%", simulated_web_nav); + registry.registerProgmemTemplate("%SIDEBAR%", simulated_web_sidebar); + registry.registerRamData("%TITLE%", getTestTitle); + registry.registerRamData("%THEME%", getTestTheme); + registry.registerRamData("%RSSI%", getTestRssi); + registry.registerRamData("%HEAP%", getTestHeap); + registry.registerRamData("%UPTIME%", getTestUptime); + registry.registerProgmemData("%STYLES%", test_css_data); + registry.registerProgmemData("%SCRIPTS%", simulated_web_scripts); + registry.registerProgmemData("%LOGO_BASE64%", simulated_logo_base64); + + TemplateContext ctx; + ctx.setRegistry(®istry); + TemplateRenderer::initializeContext(ctx, simulated_web_outer); + + const size_t chunkSize = 32; + uint8_t buffer[chunkSize]; + String rendered; + size_t iterations = 0; + + while (!TemplateRenderer::isComplete(ctx) && !ctx.hasError()) { + size_t written = TemplateRenderer::renderNextChunk(ctx, buffer, chunkSize); + iterations++; + TEST_ASSERT_FALSE_MESSAGE(ctx.hasError(), "Large template render should not error"); + TEST_ASSERT_TRUE_MESSAGE(written > 0 || TemplateRenderer::isComplete(ctx), "Renderer stalled while processing large template"); + + for (size_t i = 0; i < written; ++i) { + rendered += static_cast(buffer[i]); + } + } + + TEST_ASSERT_TRUE_MESSAGE(TemplateRenderer::isComplete(ctx), "Large template render should complete"); + TEST_ASSERT_GREATER_THAN_MESSAGE((unsigned int)1, iterations, "Large template should require multiple render iterations"); + TEST_ASSERT_TRUE_MESSAGE(rendered.length() > chunkSize, "Rendered output should be larger than a single chunk"); + + TEST_ASSERT_TRUE_MESSAGE(rendered.indexOf("
    ") >= 0, "Rendered output should include header section"); + TEST_ASSERT_TRUE_MESSAGE(rendered.indexOf("