Prechádzať zdrojové kódy

refactor(tools): rename spry-mkcomponent to spry-mkconst with new approach

Rename the spry-mkcomponent tool to spry-mkconst and change the HTML
injection approach. Instead of modifying the markup getter directly,
the tool now generates a const string that can be referenced by the
component.

This change preserves Vala LSP compatibility since the component file
remains valid Vala code with a proper const reference, rather than
relying on build-time injection that confuses language servers.

- Delete spry-mkcomponent tool and documentation
- Add spry-mkconst tool (untracked)
- Update navigation and routes to reference new tool name
- Update example component to demonstrate new workflow
- Enable tools and demo subdirectories in meson.build

BREAKING CHANGE: spry-mkcomponent has been replaced with spry-mkconst.
Projects using spry-mkcomponent will need to update their build
configuration and component structure to use the new const-based approach.
Billy Barrow 1 mesiac pred
rodič
commit
65d6983b4c

+ 2 - 2
demo/Components/NavSidebarComponent.vala

@@ -28,7 +28,7 @@ public class NavSidebarComponent : Component {
                     <li><a href="/components/actions" class="nav-item" sid="components-actions">Actions</a></li>
                     <li><a href="/components/outlets" class="nav-item" sid="components-outlets">Outlets</a></li>
                     <li><a href="/components/continuations" class="nav-item" sid="components-continuations">Continuations</a></li>
-                    <li><a href="/components/spry-mkcomponent" class="nav-item" sid="components-spry-mkcomponent">Using spry-mkcomponent</a></li>
+                    <li><a href="/components/spry-mkconst" class="nav-item" sid="components-spry-mkconst">Using spry-mkconst</a></li>
                 </ul>
             </div>
             
@@ -77,7 +77,7 @@ public class NavSidebarComponent : Component {
             case "/components/actions": return "components-actions";
             case "/components/outlets": return "components-outlets";
             case "/components/continuations": return "components-continuations";
-            case "/components/spry-mkcomponent": return "components-spry-mkcomponent";
+            case "/components/spry-mkconst": return "components-spry-mkconst";
             case "/page-components/overview": return "page-components-overview";
             case "/page-components/templates": return "page-components-templates";
             case "/static-resources/overview": return "static-resources-overview";

+ 2 - 2
demo/Main.vala

@@ -60,8 +60,8 @@ void main(string[] args) {
         application.add_transient<ComponentsContinuationsPage>();
         application.add_endpoint<ComponentsContinuationsPage>(new EndpointRoute("/components/continuations"));
         
-        application.add_transient<ComponentsMkcomponentPage>();
-        application.add_endpoint<ComponentsMkcomponentPage>(new EndpointRoute("/components/spry-mkcomponent"));
+        application.add_transient<ComponentsMkconstPage>();
+        application.add_endpoint<ComponentsMkconstPage>(new EndpointRoute("/components/spry-mkconst"));
         
         // Page Components documentation pages
         application.add_transient<PageComponentsOverviewPage>();

+ 0 - 387
demo/Pages/ComponentsMkcomponentPage.vala

@@ -1,387 +0,0 @@
-using Spry;
-using Inversion;
-
-/**
- * ComponentsMkcomponentPage - Documentation for the spry-mkcomponent tool
- * 
- * This page explains how to use spry-mkcomponent to inject HTML markup
- * into Vala component files.
- */
-public class ComponentsMkcomponentPage : PageComponent {
-    
-    public const string ROUTE = "/components/spry-mkcomponent";
-    
-    private ComponentFactory factory = inject<ComponentFactory>();
-    
-    public override string markup { get {
-        return """
-        <div sid="page" class="doc-content">
-            <h1>spry-mkcomponent Tool</h1>
-            
-            <section class="doc-section">
-                <h2>What is spry-mkcomponent?</h2>
-                <p>
-                    <code>spry-mkcomponent</code> is a command-line tool that processes Vala component
-                    files by injecting HTML markup into the <code>markup</code> property getter. This
-                    allows you to maintain your HTML templates in separate files while keeping your
-                    Vala code clean and focused on logic.
-                </p>
-                <p>
-                    The tool enables a cleaner separation of concerns:
-                </p>
-                <ul>
-                    <li><strong>Component logic</strong> - Vala code handling actions, state, and behavior</li>
-                    <li><strong>HTML templates</strong> - Separate .html files with your markup</li>
-                </ul>
-            </section>
-            
-            <section class="doc-section">
-                <h2>Command-Line Usage</h2>
-                
-                <spry-component name="CodeBlockComponent" sid="usage-code"/>
-                
-                <h3>Options</h3>
-                <table class="doc-table">
-                    <thead>
-                        <tr>
-                            <th>Option</th>
-                            <th>Description</th>
-                        </tr>
-                    </thead>
-                    <tbody>
-                        <tr>
-                            <td><code>-o, --output FILE</code></td>
-                            <td>Output file path (default: input filename in build directory)</td>
-                        </tr>
-                        <tr>
-                            <td><code>-h, --html FILE</code></td>
-                            <td>HTML template file (default: <input_vala>.html)</td>
-                        </tr>
-                        <tr>
-                            <td><code>-v, --version</code></td>
-                            <td>Show version information</td>
-                        </tr>
-                        <tr>
-                            <td><code>--help</code></td>
-                            <td>Show help message</td>
-                        </tr>
-                    </tbody>
-                </table>
-            </section>
-            
-            <section class="doc-section">
-                <h2>How It Works</h2>
-                <p>
-                    The tool takes two input files and produces a single output file with the
-                    HTML template embedded in the markup property:
-                </p>
-                
-                <spry-component name="CodeBlockComponent" sid="workflow-diagram"/>
-                
-                <div class="info-box">
-                    <p>
-                        <strong>💡 Key Concept:</strong> The tool looks for an empty <code>markup</code>
-                        property getter and injects the HTML content. If the getter already has content,
-                        the file is passed through unchanged.
-                    </p>
-                </div>
-            </section>
-            
-            <section class="doc-section">
-                <h2>Example Transformation</h2>
-                <p>
-                    Here's a complete example showing how the tool transforms your component files:
-                </p>
-                
-                <h3>Input Vala File (<code>UserContentComponent.vala</code>)</h3>
-                <spry-component name="CodeBlockComponent" sid="input-vala"/>
-                
-                <h3>Input HTML File (<code>UserContentComponent.vala.html</code>)</h3>
-                <spry-component name="CodeBlockComponent" sid="input-html"/>
-                
-                <h3>Output Vala File (<code>UserContentComponent.vala</code>)</h3>
-                <spry-component name="CodeBlockComponent" sid="output-vala"/>
-                
-                <p>
-                    Notice how the empty <code>markup</code> getter now returns the HTML content
-                    as a verbatim string literal. The <code>sid</code> attributes in your HTML
-                    are preserved, allowing you to access elements via <code>this["sid"]</code>
-                    in your Vala code.
-                </p>
-            </section>
-            
-            <section class="doc-section">
-                <h2>Integrating with Meson</h2>
-                <p>
-                    The recommended way to use spry-mkcomponent is with meson's build system.
-                    You can use either <code>generator()</code> for processing multiple files
-                    or <code>custom_target()</code> for single files with custom options.
-                </p>
-                
-                <h3>Using generator() for Multiple Files</h3>
-                <spry-component name="CodeBlockComponent" sid="meson-generator"/>
-                
-                <p>
-                    The <code>generator()</code> function is ideal when you have multiple component
-                    files that all follow the same pattern (HTML file named
-                    <code><component>.vala.html</code>).
-                </p>
-                
-                <h3>Using custom_target() for Single Files</h3>
-                <spry-component name="CodeBlockComponent" sid="meson-custom-target"/>
-                
-                <p>
-                    Use <code>custom_target()</code> when you need more control over file paths
-                    or want to specify a custom HTML template location.
-                </p>
-                
-                <h3>Complete meson.build Example</h3>
-                <spry-component name="CodeBlockComponent" sid="meson-complete"/>
-                
-                <div class="info-box">
-                    <p>
-                        <strong>💡 Tip:</strong> The <code>@INPUT@</code> and <code>@OUTPUT@</code>
-                        placeholders are automatically replaced by meson with the actual file paths.
-                        The <code>@BASENAME@</code> placeholder is replaced with the input filename
-                        without extension.
-                    </p>
-                </div>
-            </section>
-            
-            <section class="doc-section">
-                <h2>HTML Template Guidelines</h2>
-                <p>
-                    When creating HTML templates for use with spry-mkcomponent, follow these guidelines:
-                </p>
-                
-                <ul>
-                    <li>
-                        <strong>Use <code>sid</code> attributes</strong> - Add <code>sid="name"</code>
-                        to elements you need to access from Vala code via <code>this["name"]</code>
-                    </li>
-                    <li>
-                        <strong>Keep templates alongside Vala files</strong> - Name them
-                        <code>ComponentName.vala.html</code> for automatic discovery
-                    </li>
-                    <li>
-                        <strong>Use HTMX attributes</strong> - Add <code>hx-*</code> attributes for
-                        interactivity without writing JavaScript
-                    </li>
-                    <li>
-                        <strong>Avoid complex logic</strong> - Keep templates focused on structure;
-                        use Vala for conditional rendering
-                    </li>
-                </ul>
-                
-                <spry-component name="CodeBlockComponent" sid="html-guidelines"/>
-            </section>
-            
-            <section class="doc-section">
-                <h2>Workflow Integration</h2>
-                <p>
-                    A typical development workflow with spry-mkcomponent looks like this:
-                </p>
-                
-                <ol>
-                    <li><strong>Create</strong> your Vala component with an empty <code>markup</code> getter</li>
-                    <li><strong>Create</strong> an HTML template file with the same base name</li>
-                    <li><strong>Build</strong> your project - meson runs spry-mkcomponent automatically</li>
-                    <li><strong>Iterate</strong> - Edit either file; changes are picked up on rebuild</li>
-                </ol>
-                
-                <p>
-                    During development, you can run the tool manually to preview the generated output:
-                </p>
-                
-                <spry-component name="CodeBlockComponent" sid="manual-run"/>
-            </section>
-            
-            <section class="doc-section">
-                <h2>When to Use spry-mkcomponent</h2>
-                
-                <h3>✅ Good Use Cases</h3>
-                <ul>
-                    <li>Components with substantial HTML markup</li>
-                    <li>Teams where designers work on HTML separately from developers</li>
-                    <li>Reusing HTML templates across multiple components</li>
-                    <li>Keeping Vala files focused on logic</li>
-                </ul>
-                
-                <h3>❌ When to Skip It</h3>
-                <ul>
-                    <li>Simple components with minimal markup</li>
-                    <li>Quick prototypes where separation isn't beneficial</li>
-                    <li>Components with dynamically generated markup</li>
-                </ul>
-                
-                <div class="info-box">
-                    <p>
-                        <strong>💡 Remember:</strong> You can always inline markup directly in your
-                        Vala files. spry-mkcomponent is opt-in - use it when the separation provides
-                        value for your project.
-                    </p>
-                </div>
-            </section>
-            
-            <section class="doc-section">
-                <h2>Next Steps</h2>
-                <div class="nav-cards">
-                    <a href="/components/template-syntax" class="nav-card">
-                        <h3>← Template Syntax</h3>
-                        <p>Learn about component template syntax</p>
-                    </a>
-                    <a href="/components/actions" class="nav-card">
-                        <h3>Actions →</h3>
-                        <p>Handle user interactions in components</p>
-                    </a>
-                </div>
-            </section>
-        </div>
-        """;
-    }}
-    
-    public override async void prepare() throws Error {
-        var usage_code = get_component_child<CodeBlockComponent>("usage-code");
-        usage_code.language = "Bash";
-        usage_code.code = "spry-mkcomponent [OPTIONS] <INPUT_VALA_FILE>\n\n" +
-            "# Examples:\n" +
-            "spry-mkcomponent Components/UserComponent.vala\n" +
-            "spry-mkcomponent -o build/UserComponent.vala Components/UserComponent.vala\n" +
-            "spry-mkcomponent --html Templates/User.html Components/UserComponent.vala";
-        
-        var workflow_diagram = get_component_child<CodeBlockComponent>("workflow-diagram");
-        workflow_diagram.language = "Text";
-        workflow_diagram.code = "┌─────────────────────────────┐     ┌─────────────────────────────┐\n" +
-            "│   Component.vala           │     │   Component.vala.html       │\n" +
-            "│   (empty markup getter)    │     │   (HTML template)           │\n" +
-            "└─────────────┬───────────────┘     └─────────────┬───────────────┘\n" +
-            "              │                                   │\n" +
-            "              └───────────────┬───────────────────┘\n" +
-            "                              │\n" +
-            "                              ▼\n" +
-            "                    ┌─────────────────┐\n" +
-            "                    │ spry-mkcomponent │\n" +
-            "                    └────────┬────────┘\n" +
-            "                             │\n" +
-            "                             ▼\n" +
-            "              ┌─────────────────────────────┐\n" +
-            "              │   Component.vala            │\n" +
-            "              │   (with generated getter)   │\n" +
-            "              └─────────────────────────────┘";
-        
-        var input_vala = get_component_child<CodeBlockComponent>("input-vala");
-        input_vala.language = "Vala";
-        input_vala.code = "class UserContentComponent : Component {\n" +
-            "    public override string markup { get; }\n\n" +
-            "    public async override void handle_action(string action) throws Error {\n" +
-            "        this[\"message\"].text_content = \"Hello, World!\";\n" +
-            "    }\n" +
-            "}";
-        
-        var input_html = get_component_child<CodeBlockComponent>("input-html");
-        input_html.language = "HTML";
-        input_html.code = "<p>You said: <strong sid=\"message\"></strong></p>";
-        
-        var output_vala = get_component_child<CodeBlockComponent>("output-vala");
-        output_vala.language = "Vala";
-        output_vala.code = "class UserContentComponent : Component {\n" +
-            "    public override string markup { get {\n" +
-            "        return \"\"\"<p>You said: <strong sid=\"message\"></strong></p>\"\"\";\n" +
-            "    }}\n\n" +
-            "    public async override void handle_action(string action) throws Error {\n" +
-            "        this[\"message\"].text_content = \"Hello, World!\";\n" +
-            "    }\n" +
-            "}";
-        
-        var meson_generator = get_component_child<CodeBlockComponent>("meson-generator");
-        meson_generator.language = "Meson";
-        meson_generator.code = "# Find the tool\n" +
-            "spry_mkcomponent = find_program('spry-mkcomponent')\n\n" +
-            "# Define the generator once\n" +
-            "spry_mkcomponent_gen = generator(spry_mkcomponent,\n" +
-            "    output: '@BASENAME@.vala',\n" +
-            "    arguments: ['@INPUT@', '@OUTPUT@']\n" +
-            ")\n\n" +
-            "# Process multiple component files\n" +
-            "generated_components = spry_mkcomponent_gen.process(files(\n" +
-            "    'Components/UserContentComponent.vala',\n" +
-            "    'Components/LoginFormComponent.vala',\n" +
-            "    'Components/NavSidebarComponent.vala',\n" +
-            "))\n\n" +
-            "# Use in executable\n" +
-            "executable('myapp',\n" +
-            "    sources: [other_sources, generated_components],\n" +
-            "    dependencies: [spry_dep]\n" +
-            ")";
-        
-        var meson_custom_target = get_component_child<CodeBlockComponent>("meson-custom-target");
-        meson_custom_target.language = "Meson";
-        meson_custom_target.code = "# Simple case - HTML file is ComponentName.vala.html\n" +
-            "user_content_component = custom_target('user-content-component',\n" +
-            "    input: 'Components/UserContentComponent.vala',\n" +
-            "    output: 'UserContentComponent.vala',\n" +
-            "    command: [spry_mkcomponent, '@INPUT@', '@OUTPUT@']\n" +
-            ")\n\n" +
-            "# With explicit HTML file path\n" +
-            "login_form_component = custom_target('login-form-component',\n" +
-            "    input: 'Components/LoginFormComponent.vala',\n" +
-            "    output: 'LoginFormComponent.vala',\n" +
-            "    command: [spry_mkcomponent, '@INPUT@', '@OUTPUT@',\n" +
-            "              '--html', 'Templates/LoginForm.html']\n" +
-            ")";
-        
-        var meson_complete = get_component_child<CodeBlockComponent>("meson-complete");
-        meson_complete.language = "Meson";
-        meson_complete.code = "# Find the spry-mkcomponent tool\n" +
-            "spry_mkcomponent = find_program('spry-mkcomponent')\n\n" +
-            "# Define generator for components\n" +
-            "component_gen = generator(spry_mkcomponent,\n" +
-            "    output: '@BASENAME@.vala',\n" +
-            "    arguments: ['@INPUT@', '@OUTPUT@']\n" +
-            ")\n\n" +
-            "# Process all component files with HTML templates\n" +
-            "generated_components = component_gen.process(files(\n" +
-            "    'Components/HeaderComponent.vala',\n" +
-            "    'Components/FooterComponent.vala',\n" +
-            "    'Components/SidebarComponent.vala',\n" +
-            "))\n\n" +
-            "# Other source files (no HTML templates)\n" +
-            "other_sources = files(\n" +
-            "    'Main.vala',\n" +
-            "    'Components/SimpleComponent.vala',  # Inline markup\n" +
-            ")\n\n" +
-            "# Build executable with generated components\n" +
-            "executable('myapp',\n" +
-            "    sources: [other_sources, generated_components],\n" +
-            "    dependencies: [spry_dep],\n" +
-            "    vala_args: ['--vapidir', spry_vapi_dir]\n" +
-            ")";
-        
-        var html_guidelines = get_component_child<CodeBlockComponent>("html-guidelines");
-        html_guidelines.language = "HTML";
-        html_guidelines.code = "<!-- Good: Use sid attributes for elements you need to access -->\n" +
-            "<div class=\"user-card\">\n" +
-            "    <h2 sid=\"username\"></h2>\n" +
-            "    <p sid=\"email\"></p>\n" +
-            "    <button sid=\"delete-btn\" hx-post=\"/delete\" hx-target=\"closest .user-card\">\n" +
-            "        Delete\n" +
-            "    </button>\n" +
-            "</div>\n\n" +
-            "<!-- Good: Keep it simple and focused on structure -->\n" +
-            "<form sid=\"login-form\" hx-post=\"/login\">\n" +
-            "    <input type=\"text\" name=\"username\" sid=\"username-input\"/>\n" +
-            "    <input type=\"password\" name=\"password\" sid=\"password-input\"/>\n" +
-            "    <button type=\"submit\">Login</button>\n" +
-            "</form>";
-        
-        var manual_run = get_component_child<CodeBlockComponent>("manual-run");
-        manual_run.language = "Bash";
-        manual_run.code = "# Run manually to see generated output\n" +
-            "spry-mkcomponent Components/MyComponent.vala -o /tmp/output.vala\n\n" +
-            "# View the result\n" +
-            "cat /tmp/output.vala\n\n" +
-            "# Check if the tool would modify a file (compare with diff)\n" +
-            "spry-mkcomponent Components/MyComponent.vala -o /tmp/test.vala && \\\n" +
-            "    diff Components/MyComponent.vala /tmp/test.vala";
-    }
-}

+ 391 - 0
demo/Pages/ComponentsMkconstPage.vala

@@ -0,0 +1,391 @@
+using Spry;
+using Inversion;
+
+/**
+ * ComponentsMkconstPage - Documentation for the spry-mkconst tool
+ * 
+ * This page explains how to use spry-mkconst to convert HTML files
+ * to Vala string constants that can be referenced in component markup properties.
+ */
+public class ComponentsMkconstPage : PageComponent {
+    
+    public const string ROUTE = "/components/spry-mkconst";
+    
+    private ComponentFactory factory = inject<ComponentFactory>();
+    
+    public override string markup { get {
+        return """
+        <div sid="page" class="doc-content">
+            <h1>spry-mkconst Tool</h1>
+            
+            <section class="doc-section">
+                <h2>What is spry-mkconst?</h2>
+                <p>
+                    <code>spry-mkconst</code> is a command-line tool that converts file contents
+                    (typically HTML templates) into Vala string constants. This allows you to:
+                </p>
+                <ul>
+                    <li>Keep HTML templates in separate files for easier editing</li>
+                    <li>Maintain valid Vala syntax that the Vala language server can recognize</li>
+                    <li>Reference generated constants from your component's <code>markup</code> property</li>
+                </ul>
+                <p>
+                    Unlike the previous <code>spry-mkcomponent</code> approach which modified Vala files,
+                    <code>spry-mkconst</code> generates separate constant declarations, keeping your
+                    original Vala files intact and recognizable by IDEs and language servers.
+                </p>
+            </section>
+            
+            <section class="doc-section">
+                <h2>Command-Line Usage</h2>
+                
+                <spry-component name="CodeBlockComponent" sid="usage-code"/>
+                
+                <h3>Options</h3>
+                <table class="doc-table">
+                    <thead>
+                        <tr>
+                            <th>Option</th>
+                            <th>Description</th>
+                        </tr>
+                    </thead>
+                    <tbody>
+                        <tr>
+                            <td><code>-o, --output FILE</code></td>
+                            <td>Output file path (default: stdout)</td>
+                        </tr>
+                        <tr>
+                            <td><code>--ns NAMESPACE</code></td>
+                            <td>Wrap const in a namespace</td>
+                        </tr>
+                        <tr>
+                            <td><code>-n, --name NAME</code></td>
+                            <td>Override const name (default: derived from filename)</td>
+                        </tr>
+                        <tr>
+                            <td><code>-v, --version</code></td>
+                            <td>Show version information</td>
+                        </tr>
+                        <tr>
+                            <td><code>--help</code></td>
+                            <td>Show help message</td>
+                        </tr>
+                    </tbody>
+                </table>
+            </section>
+            
+            <section class="doc-section">
+                <h2>How It Works</h2>
+                <p>
+                    The tool takes a single input file and generates a Vala const declaration:
+                </p>
+                
+                <spry-component name="CodeBlockComponent" sid="workflow-diagram"/>
+                
+                <div class="info-box">
+                    <p>
+                        <strong>💡 Key Concept:</strong> The const name is automatically derived from the
+                        filename using CAPITAL_SNAKE_CASE. For example, <code>UserContentComponent.vala.html</code>
+                        becomes <code>USER_CONTENT_COMPONENT_VALA_HTML</code>.
+                    </p>
+                </div>
+            </section>
+            
+            <section class="doc-section">
+                <h2>Example Transformation</h2>
+                <p>
+                    Here's a complete example showing how the tool works:
+                </p>
+                
+                <h3>Input HTML File (<code>UserContentComponent.vala.html</code>)</h3>
+                <spry-component name="CodeBlockComponent" sid="input-html"/>
+                
+                <h3>Generated Output (stdout or file)</h3>
+                <spry-component name="CodeBlockComponent" sid="output-const"/>
+                
+                <h3>Your Vala Component (unchanged, valid Vala)</h3>
+                <spry-component name="CodeBlockComponent" sid="vala-component"/>
+                
+                <p>
+                    The key advantage is that your Vala file remains valid, parseable Vala code.
+                    The Vala language server can still recognize it, provide autocomplete, and
+                    show errors. The generated constant is referenced via <code>return USER_CONTENT_COMPONENT_VALA_HTML;</code>
+                </p>
+            </section>
+            
+            <section class="doc-section">
+                <h2>Integrating with Meson</h2>
+                <p>
+                    The recommended way to use spry-mkconst is with meson's build system.
+                    You generate a separate Vala file containing the const declarations,
+                    then include it in your build.
+                </p>
+                
+                <h3>Basic meson.build Setup</h3>
+                <spry-component name="CodeBlockComponent" sid="meson-basic"/>
+                
+                <h3>With Namespace</h3>
+                <spry-component name="CodeBlockComponent" sid="meson-namespace"/>
+                
+                <h3>Complete Example</h3>
+                <spry-component name="CodeBlockComponent" sid="meson-complete"/>
+                
+                <div class="info-box">
+                    <p>
+                        <strong>💡 Tip:</strong> The generated file should be included in your
+                        executable's sources. Meson will run spry-mkconst automatically before
+                        compilation.
+                    </p>
+                </div>
+            </section>
+            
+            <section class="doc-section">
+                <h2>HTML Template Guidelines</h2>
+                <p>
+                    When creating HTML templates for use with spry-mkconst, follow these guidelines:
+                </p>
+                
+                <ul>
+                    <li>
+                        <strong>Use <code>sid</code> attributes</strong> - Add <code>sid="name"</code>
+                        to elements you need to access from Vala code via <code>this["name"]</code>
+                    </li>
+                    <li>
+                        <strong>Name templates consistently</strong> - Use <code>ComponentName.vala.html</code>
+                        for clear association with the component
+                    </li>
+                    <li>
+                        <strong>Use HTMX attributes</strong> - Add <code>hx-*</code> attributes for
+                        interactivity without writing JavaScript
+                    </li>
+                    <li>
+                        <strong>Avoid complex logic</strong> - Keep templates focused on structure;
+                        use Vala for conditional rendering
+                    </li>
+                </ul>
+                
+                <spry-component name="CodeBlockComponent" sid="html-guidelines"/>
+            </section>
+            
+            <section class="doc-section">
+                <h2>Why spry-mkconst vs spry-mkcomponent?</h2>
+                <p>
+                    The previous <code>spry-mkcomponent</code> tool modified Vala source files by
+                    injecting HTML into the markup getter. This had a significant drawback:
+                    the Vala language server couldn't recognize the modified files as valid Vala
+                    during development.
+                </p>
+                
+                <p>
+                    <code>spry-mkconst</code> solves this by:
+                </p>
+                
+                <ul>
+                    <li>Keeping your Vala files as valid, parseable Vala code</li>
+                    <li>Generating constants in a separate file that's included at build time</li>
+                    <li>Allowing the language server to provide full IDE support</li>
+                    <li>Using <code>public override string markup { get { return CONSTANT_NAME; } }</code>
+                        which is valid Vala syntax</li>
+                </ul>
+            </section>
+            
+            <section class="doc-section">
+                <h2>Workflow Integration</h2>
+                <p>
+                    A typical development workflow with spry-mkconst looks like this:
+                </p>
+                
+                <ol>
+                    <li><strong>Create</strong> your HTML template file (e.g., <code>MyComponent.vala.html</code>)</li>
+                    <li><strong>Write</strong> your Vala component referencing the generated constant name</li>
+                    <li><strong>Configure</strong> meson to generate the const file</li>
+                    <li><strong>Build</strong> your project - meson runs spry-mkconst automatically</li>
+                    <li><strong>Iterate</strong> - Edit the HTML file; changes are picked up on rebuild</li>
+                </ol>
+                
+                <p>
+                    During development, you can run the tool manually to see the generated output:
+                </p>
+                
+                <spry-component name="CodeBlockComponent" sid="manual-run"/>
+            </section>
+            
+            <section class="doc-section">
+                <h2>When to Use spry-mkconst</h2>
+                
+                <h3>✅ Good Use Cases</h3>
+                <ul>
+                    <li>Components with substantial HTML markup</li>
+                    <li>When you need Vala language server support (autocomplete, errors)</li>
+                    <li>Teams where designers work on HTML separately from developers</li>
+                    <li>Keeping Vala files focused on logic</li>
+                </ul>
+                
+                <h3>❌ When to Skip It</h3>
+                <ul>
+                    <li>Simple components with minimal markup (inline is fine)</li>
+                    <li>Quick prototypes where separation isn't beneficial</li>
+                    <li>Components with dynamically generated markup</li>
+                </ul>
+                
+                <div class="info-box">
+                    <p>
+                        <strong>💡 Remember:</strong> You can always inline markup directly in your
+                        Vala files using triple-quoted strings. spry-mkconst is opt-in - use it
+                        when the separation provides value for your project.
+                    </p>
+                </div>
+            </section>
+            
+            <section class="doc-section">
+                <h2>Next Steps</h2>
+                <div class="nav-cards">
+                    <a href="/components/template-syntax" class="nav-card">
+                        <h3>← Template Syntax</h3>
+                        <p>Learn about component template syntax</p>
+                    </a>
+                    <a href="/components/actions" class="nav-card">
+                        <h3>Actions →</h3>
+                        <p>Handle user interactions in components</p>
+                    </a>
+                </div>
+            </section>
+        </div>
+        """;
+    }}
+    
+    public override async void prepare() throws Error {
+        var usage_code = get_component_child<CodeBlockComponent>("usage-code");
+        usage_code.language = "Bash";
+        usage_code.code = "spry-mkconst [OPTIONS] <INPUT_FILE>\n\n" +
+            "# Examples:\n" +
+            "spry-mkconst Templates/UserContent.html\n" +
+            "spry-mkconst -o generated/constants.vala Templates/UserContent.html\n" +
+            "spry-mkconst --ns MyApp.Templates Templates/UserContent.html\n" +
+            "spry-mkconst -n CUSTOM_NAME Templates/UserContent.html";
+        
+        var workflow_diagram = get_component_child<CodeBlockComponent>("workflow-diagram");
+        workflow_diagram.language = "Text";
+        workflow_diagram.code = "┌─────────────────────────────────────┐\n" +
+            "│   UserContentComponent.vala.html   │\n" +
+            "│   (HTML template)                  │\n" +
+            "└─────────────┬───────────────────────┘\n" +
+            "              │\n" +
+            "              ▼\n" +
+            "        ┌───────────────┐\n" +
+            "        │ spry-mkconst  │\n" +
+            "        └───────┬───────┘\n" +
+            "                │\n" +
+            "                ▼\n" +
+            "┌─────────────────────────────────────────────────┐\n" +
+            "│ const string USER_CONTENT_COMPONENT_VALA_HTML   │\n" +
+            "│     = \"\"\"<html>...</html>\"\"\";                   │\n" +
+            "└─────────────────────────────────────────────────┘";
+        
+        var input_html = get_component_child<CodeBlockComponent>("input-html");
+        input_html.language = "HTML";
+        input_html.code = "<p>You said: <strong sid=\"message\"></strong> <em>(via <span sid=\"action\"></span>)</em></p>";
+        
+        var output_const = get_component_child<CodeBlockComponent>("output-const");
+        output_const.language = "Vala";
+        output_const.code = "const string USER_CONTENT_COMPONENT_VALA_HTML = \"\"\"\n" +
+            "<p>You said: <strong sid=\"message\"></strong> <em>(via <span sid=\"action\"></span>)</em></p>\n" +
+            "\"\"\";";
+        
+        var vala_component = get_component_child<CodeBlockComponent>("vala-component");
+        vala_component.language = "Vala";
+        vala_component.code = "// This file is valid Vala - the language server recognizes it!\n" +
+            "class UserContentComponent : Component {\n" +
+            "    public override string markup { get { return USER_CONTENT_COMPONENT_VALA_HTML; } }\n\n" +
+            "    public async override void handle_action(string action) throws Error {\n" +
+            "        this[\"message\"].text_content = \"Hello, World!\";\n" +
+            "    }\n" +
+            "}";
+        
+        var meson_basic = get_component_child<CodeBlockComponent>("meson-basic");
+        meson_basic.language = "Meson";
+        meson_basic.code = "# Find the tool\n" +
+            "spry_mkconst = find_program('spry-mkconst')\n\n" +
+            "# Generate constants from HTML templates\n" +
+            "markup_constants = custom_target('markup-constants',\n" +
+            "    input: 'Templates/UserContentComponent.vala.html',\n" +
+            "    output: 'UserContentConstants.vala',\n" +
+            "    command: [spry_mkconst, '@INPUT@', '-o', '@OUTPUT@']\n" +
+            ")\n\n" +
+            "# Include in your executable\n" +
+            "executable('myapp',\n" +
+            "    sources: ['Main.vala', 'UserContentComponent.vala', markup_constants],\n" +
+            "    dependencies: [spry_dep]\n" +
+            ")";
+        
+        var meson_namespace = get_component_child<CodeBlockComponent>("meson-namespace");
+        meson_namespace.language = "Meson";
+        meson_namespace.code = "# Generate constants with namespace\n" +
+            "spry_mkconst = find_program('spry-mkconst')\n\n" +
+            "markup_constants = custom_target('markup-constants',\n" +
+            "    input: 'Templates/UserContentComponent.vala.html',\n" +
+            "    output: 'UserContentConstants.vala',\n" +
+            "    command: [spry_mkconst, '@INPUT@', '-o', '@OUTPUT@',\n" +
+            "              '--ns', 'MyApp.Markup']\n" +
+            ")\n\n" +
+            "# In your Vala file, use:\n" +
+            "# public override string markup { get { return MyApp.Markup.USER_CONTENT_COMPONENT_VALA_HTML; } }";
+        
+        var meson_complete = get_component_child<CodeBlockComponent>("meson-complete");
+        meson_complete.language = "Meson";
+        meson_complete.code = "# Find the spry-mkconst tool\n" +
+            "spry_mkconst = find_program('spry-mkconst')\n\n" +
+            "# Collect all HTML template files\n" +
+            "template_files = files(\n" +
+            "    'Templates/HeaderComponent.vala.html',\n" +
+            "    'Templates/FooterComponent.vala.html',\n" +
+            "    'Templates/UserContentComponent.vala.html',\n" +
+            ")\n\n" +
+            "# Generate a single constants file from all templates\n" +
+            "# Note: You can also generate separate files per template\n" +
+            "markup_constants = custom_target('markup-constants',\n" +
+            "    input: template_files,\n" +
+            "    output: 'MarkupConstants.vala',\n" +
+            "    command: [spry_mkconst, '@INPUT@', '-o', '@OUTPUT@', '--ns', 'Markup']\n" +
+            ")\n\n" +
+            "# Component source files (reference the constants)\n" +
+            "component_sources = files(\n" +
+            "    'Components/HeaderComponent.vala',\n" +
+            "    'Components/FooterComponent.vala',\n" +
+            "    'Components/UserContentComponent.vala',\n" +
+            ")\n\n" +
+            "# Build executable\n" +
+            "executable('myapp',\n" +
+            "    sources: ['Main.vala', component_sources, markup_constants],\n" +
+            "    dependencies: [spry_dep],\n" +
+            ")";
+        
+        var html_guidelines = get_component_child<CodeBlockComponent>("html-guidelines");
+        html_guidelines.language = "HTML";
+        html_guidelines.code = "<!-- Good: Use sid attributes for elements you need to access -->\n" +
+            "<div class=\"user-card\">\n" +
+            "    <h2 sid=\"username\"></h2>\n" +
+            "    <p sid=\"email\"></p>\n" +
+            "    <button sid=\"delete-btn\" hx-post=\"/delete\" hx-target=\"closest .user-card\">\n" +
+            "        Delete\n" +
+            "    </button>\n" +
+            "</div>\n\n" +
+            "<!-- Good: Keep it simple and focused on structure -->\n" +
+            "<form sid=\"login-form\" hx-post=\"/login\">\n" +
+            "    <input type=\"text\" name=\"username\" sid=\"username-input\"/>\n" +
+            "    <input type=\"password\" name=\"password\" sid=\"password-input\"/>\n" +
+            "    <button type=\"submit\">Login</button>\n" +
+            "</form>";
+        
+        var manual_run = get_component_child<CodeBlockComponent>("manual-run");
+        manual_run.language = "Bash";
+        manual_run.code = "# Run manually to see generated output\n" +
+            "spry-mkconst Templates/MyComponent.vala.html\n\n" +
+            "# Save to a file\n" +
+            "spry-mkconst Templates/MyComponent.vala.html -o generated/MyComponentMarkup.vala\n\n" +
+            "# View the result\n" +
+            "cat generated/MyComponentMarkup.vala\n\n" +
+            "# Generate with namespace\n" +
+            "spry-mkconst Templates/MyComponent.vala.html --ns MyApp.Templates";
+    }
+}

+ 1 - 1
demo/meson.build

@@ -13,7 +13,7 @@ website_sources = files(
     'Pages/PageComponentsOverviewPage.vala',
     'Pages/PageTemplatesPage.vala',
     'Pages/StaticResourcesOverviewPage.vala',
-    'Pages/ComponentsMkcomponentPage.vala',
+    'Pages/ComponentsMkconstPage.vala',
     'Pages/StaticResourcesMkssrPage.vala',
     'Components/CodeBlockComponent.vala',
     'Components/DemoHostComponent.vala',

+ 11 - 1
examples/UserContentComponent.vala

@@ -1,5 +1,10 @@
+// This file demonstrates the spry-mkconst workflow:
+// 1. The HTML template (UserContentComponent.vala.html) is converted to a const using spry-mkconst
+// 2. The generated const (USER_CONTENT_COMPONENT_VALA_HTML) is referenced in the markup property
+// 3. This allows the Vala language server to still recognize this file as valid Vala code
+
 class UserContentComponent : Component {
-    public override string markup { get; }
+    public override string markup { get { return USER_CONTENT_COMPONENT_VALA_HTML; } }
 
     private HttpContext http_context = inject<HttpContext>();
 
@@ -8,3 +13,8 @@ class UserContentComponent : Component {
         this["action"].text_content = action;
     }
 }
+
+// Generated by spry-mkconst (would be in a separate file or included via build system):
+// const string USER_CONTENT_COMPONENT_VALA_HTML = """
+// <p>You said: <strong sid="message"></strong> <em>(via <span sid="action"></span>)</em></p>
+// """;

+ 2 - 2
meson.build

@@ -26,6 +26,6 @@ add_project_arguments(['--vapidir', vapi_dir], language: 'vala')
 
 subdir('src')
 subdir('examples')
-# subdir('tools')
+subdir('tools')
 # subdir('website')
-# subdir('demo')
+subdir('demo')

+ 0 - 251
plans/spry-mkcomponent-design.md

@@ -1,251 +0,0 @@
-# spry-mkcomponent Tool Design
-
-## Overview
-
-A new tool called `spry-mkcomponent` that processes Vala component files by injecting HTML markup into the `markup` property getter. This separates the HTML template from the Vala code, making components easier to maintain.
-
-## Tool Behavior
-
-### Input/Output
-
-```
-Input:  UserContentComponent.vala      (Vala source with empty markup getter)
-        UserContentComponent.vala.html (HTML template - auto-discovered)
-
-Output: UserContentComponent.vala      (Generated Vala with markup getter populated)
-```
-
-### Command Line Interface
-
-```bash
-spry-mkcomponent [OPTIONS] <INPUT_VALA_FILE>
-
-Options:
-  -o, --output FILE         Output file path (default: input filename in build directory)
-  -h, --html FILE           HTML template file (default: <input_vala>.html)
-  -v, --version             Show version information
-  --help                    Show help message
-```
-
-### Processing Logic
-
-1. Read the input `.vala` file
-2. Locate the HTML template file:
-   - If `-h/--html` is specified, use that path
-   - Otherwise, append `.html` to the input vala file path
-3. Parse the Vala file to find the `markup` property with an empty getter:
-   - Pattern: `public override string markup { get; }`
-   - The property MUST have exactly this form (empty getter)
-4. Read the HTML template content
-5. Generate the output by replacing the empty getter with a getter that returns the HTML as a verbatim string
-6. Write the output file
-
-### Error Conditions
-
-| Condition | Error Message |
-|-----------|---------------|
-| Input file not found | `Error: Input file 'PATH' does not exist` |
-| HTML file not found | `Error: HTML template file 'PATH' does not exist` |
-| No markup property found | `Error: No 'public override string markup' property found in 'PATH'` |
-| Markup property has non-empty getter | `Error: The markup property in 'PATH' already has a getter defined. Expected empty getter: { get; }` |
-
-## Example Transformation
-
-### Input: [`UserContentComponent.vala`](examples/UserContentComponent.vala)
-
-```vala
-class UserContentComponent : Component {
-    public override string markup { get; }
-
-    private HttpContext http_context = inject<HttpContext>();
-
-    public async override void handle_action(string action) throws Error {
-        this["message"].text_content = http_context.request.query_params.get_any_or_default("message") ?? "No message provided!";
-        this["action"].text_content = action;
-    }
-}
-```
-
-### Input: [`UserContentComponent.vala.html`](examples/UserContentComponent.vala.html)
-
-```html
-<p>You said: <strong sid="message"></strong> <em>(via <span sid="action"></span>)</em></p>
-```
-
-### Output: `UserContentComponent.vala` (generated)
-
-```vala
-class UserContentComponent : Component {
-    public override string markup { get {
-        return """
-        <p>You said: <strong sid="message"></strong> <em>(via <span sid="action"></span>)</em></p>
-        """;
-    }}
-
-    private HttpContext http_context = inject<HttpContext>();
-
-    public async override void handle_action(string action) throws Error {
-        this["message"].text_content = http_context.request.query_params.get_any_or_default("message") ?? "No message provided!";
-        this["action"].text_content = action;
-    }
-}
-```
-
-## Meson Integration: generator() vs custom_target()
-
-### generator()
-
-**Best for:** Multiple files with the same transformation pattern.
-
-**Pros:**
-- Reusable - define once, use for many files
-- Cleaner syntax when processing multiple components
-- Meson automatically handles output file naming
-
-**Cons:**
-- Less flexibility per-file (same command arguments for all files)
-- Output filename is automatically determined
-
-**Example Usage:**
-
-```meson
-# Define the generator once
-spry_mkcomponent_gen = generator(spry_mkcomponent,
-    output: '@BASENAME@.vala',
-    arguments: ['@INPUT@', '@OUTPUT@']
-)
-
-# Process multiple component files
-generated_components = spry_mkcomponent_gen.process(files(
-    'Components/UserContentComponent.vala',
-    'Components/LoginFormComponent.vala',
-    'Components/NavSidebarComponent.vala',
-))
-
-# Use in executable
-executable('myapp',
-    sources: [other_sources, generated_components],
-    dependencies: [spry_dep]
-)
-```
-
-### custom_target()
-
-**Best for:** Single files or when you need per-file customization.
-
-**Pros:**
-- Full control over command arguments per file
-- Can specify custom output filename
-- Better for complex dependencies
-
-**Cons:**
-- More verbose for multiple files
-- Each file needs its own custom_target declaration
-
-**Example Usage:**
-
-```meson
-# Single component with custom HTML path
-user_content_component = custom_target('user-content-component',
-    input: 'Components/UserContentComponent.vala',
-    output: 'UserContentComponent.vala',
-    command: [spry_mkcomponent, '@INPUT@', '@OUTPUT@']
-)
-
-# With explicit HTML file
-login_form_component = custom_target('login-form-component',
-    input: 'Components/LoginFormComponent.vala',
-    output: 'LoginFormComponent.vala',
-    command: [spry_mkcomponent, '@INPUT@', '@OUTPUT@', '--html', 'Templates/LoginForm.html']
-)
-
-executable('myapp',
-    sources: [user_content_component, login_form_component, other_sources],
-    dependencies: [spry_dep]
-)
-```
-
-### Recommendation
-
-For this use case, I recommend **`generator()`** as the primary pattern because:
-
-1. Components typically follow the same convention (`.vala` + `.vala.html`)
-2. Most use cases will process multiple component files
-3. The pattern matches how `spry-mkssr` is used in the demo build
-
-However, the tool should support both patterns by:
-- Accepting input/output as positional arguments (for generator)
-- Supporting `--html` flag for explicit HTML file paths (for custom_target when needed)
-
-## Implementation Architecture
-
-```mermaid
-flowchart TD
-    A[Command Line Args] --> B[Argument Parser]
-    B --> C[File Discovery]
-    C --> D{HTML file specified?}
-    D -->|Yes| E[Use specified HTML path]
-    D -->|No| F[Append .html to vala path]
-    E --> G[Read Input Files]
-    F --> G
-    G --> H[Parse Vala Source]
-    H --> I{Find markup property}
-    I -->|Found| J{Is getter empty?}
-    I -->|Not Found| K[Error: No markup property]
-    J -->|Yes| L[Generate Output]
-    J -->|No| M[Error: Getter not empty]
-    L --> N[Write Output File]
-    N --> O[Success]
-```
-
-## File Structure
-
-```
-tools/
-├── meson.build
-└── spry-mkcomponent/
-    ├── meson.build
-    └── spry-mkcomponent.vala
-```
-
-## Dependencies
-
-The tool will need:
-- `glib-2.0` - Core GLib functionality
-- `gobject-2.0` - GObject type system
-- `gio-2.0` - File I/O operations
-
-Note: Unlike `spry-mkssr`, this tool does NOT need `invercargill`, `astralis`, `inversion`, or compression libraries since it only does text processing.
-
-## Implementation Tasks
-
-1. Create `tools/spry-mkcomponent/` directory structure
-2. Implement `spry-mkcomponent.vala` with:
-   - Command-line argument parsing using `OptionContext`
-   - File reading/writing using `gio`
-   - Vala source parsing to find `markup` property
-   - HTML template injection
-   - Error handling with descriptive messages
-3. Create `tools/spry-mkcomponent/meson.build`
-4. Update `tools/meson.build` to include the new subdirectory
-5. Add example usage documentation
-
-## Parsing Strategy
-
-The Vala source parsing will use a simple regex-based approach:
-
-1. Search for pattern: `public\s+override\s+string\s+markup\s*\{\s*get\s*;\s*\}`
-2. Replace with the generated getter containing the HTML
-
-This is sufficient because:
-- The pattern is well-defined and consistent
-- Full Vala parsing would add unnecessary complexity
-- The tool validates that the property exists and has an empty getter
-
-### Edge Cases to Handle
-
-- Multiple spaces/tabs between keywords
-- Different indentation styles
-- Windows vs Unix line endings (normalize to Unix)
-- Preserve original indentation in output
-- Escape triple quotes in HTML content (rare but possible)

+ 1 - 1
tools/meson.build

@@ -1,4 +1,4 @@
 # Spry Tools
 
 subdir('spry-mkssr')
-subdir('spry-mkcomponent')
+subdir('spry-mkconst')

+ 0 - 9
tools/spry-mkcomponent/meson.build

@@ -1,9 +0,0 @@
-spry_mkcomponent_sources = files(
-    'spry-mkcomponent.vala'
-)
-
-spry_mkcomponent = executable('spry-mkcomponent',
-    spry_mkcomponent_sources,
-    dependencies: [glib_dep, gobject_dep, gio_dep],
-    install: true
-)

+ 0 - 227
tools/spry-mkcomponent/spry-mkcomponent.vala

@@ -1,227 +0,0 @@
-namespace Spry.Tools {
-
-    public class Mkcomponent : Object {
-
-        private static bool show_version = false;
-        private static string? output_file = null;
-        private static string? html_file = null;
-
-        private const OptionEntry[] options = {
-            { "output", 'o', 0, OptionArg.FILENAME, ref output_file, "Output file path (alternative to positional OUTPUT_FILE)", "FILE" },
-            { "html", 'H', 0, OptionArg.FILENAME, ref html_file, "HTML template file (default: <input_vala>.html)", "FILE" },
-            { "version", 'v', 0, OptionArg.NONE, ref show_version, "Show version information", null },
-            { null }
-        };
-
-        public static int main(string[] args) {
-            try {
-                var opt_context = new OptionContext("<INPUT_VALA_FILE> [OUTPUT_FILE] - Inject HTML markup into Vala component files");
-                opt_context.set_help_enabled(true);
-                opt_context.add_main_entries(options, null);
-                opt_context.set_description(
-                    "Arguments:\n" +
-                    "  INPUT_VALA_FILE         Input Vala component file\n" +
-                    "  OUTPUT_FILE             Output Vala file (optional, default: input filename in build directory)\n" +
-                    "\n" +
-                    "The output file can be specified either as a positional argument or via -o/--output."
-                );
-                opt_context.parse(ref args);
-            } catch (OptionError e) {
-                stderr.printf("Error: %s\n", e.message);
-                stderr.printf("Run '%s --help' for more information.\n", args[0]);
-                return 1;
-            }
-
-            if (show_version) {
-                stdout.printf("spry-mkcomponent 0.1\n");
-                return 0;
-            }
-
-            // Get input file and optional output file from remaining arguments
-            string? input_file = null;
-            string? positional_output = null;
-            
-            if (args.length > 1) {
-                input_file = args[1];
-            }
-            if (args.length > 2) {
-                positional_output = args[2];
-            }
-
-            if (input_file == null) {
-                stderr.printf("Error: No input file specified.\n");
-                stderr.printf("Run '%s --help' for more information.\n", args[0]);
-                return 1;
-            }
-
-            // Determine output file: -o flag takes precedence over positional argument
-            string actual_output;
-            if (output_file != null) {
-                actual_output = output_file;
-            } else if (positional_output != null) {
-                actual_output = positional_output;
-            } else {
-                // Default: use the input filename (will be written to build directory by meson)
-                actual_output = input_file;
-            }
-
-            // Determine HTML file
-            string actual_html;
-            if (html_file != null) {
-                actual_html = html_file;
-            } else {
-                // Default: append .html to the input vala file path
-                actual_html = input_file + ".html";
-            }
-
-            try {
-                var tool = new Mkcomponent();
-                tool.process(input_file, actual_output, actual_html);
-                return 0;
-            } catch (Error e) {
-                stderr.printf("%s\n", e.message);
-                return 1;
-            }
-        }
-
-        public void process(string input_path, string output_path, string html_path) throws Error {
-            // Check if input file exists
-            var input_file = File.new_for_path(input_path);
-            if (!input_file.query_exists()) {
-                throw new IOError.NOT_FOUND(@"Error: Input file '$(input_path)' does not exist");
-            }
-
-            // Check if HTML file exists
-            var html_file_obj = File.new_for_path(html_path);
-            if (!html_file_obj.query_exists()) {
-                throw new IOError.NOT_FOUND(@"Error: HTML template file '$(html_path)' does not exist");
-            }
-
-            // Read input Vala file
-            string vala_content = read_file_content(input_path);
-
-            // Read HTML template
-            string html_content = read_file_content(html_path);
-
-            // Normalize line endings to Unix style
-            vala_content = vala_content.replace("\r\n", "\n").replace("\r", "\n");
-            html_content = html_content.replace("\r\n", "\n").replace("\r", "\n");
-
-            // First check if there's a markup property with a getter body (to give specific error)
-            Regex getter_with_body_regex;
-            try {
-                // This pattern matches a markup property with a getter that has a body
-                getter_with_body_regex = new Regex(
-                    @"public\\s+override\\s+string\\s+markup\\s*\\{\\s*get\\s*\\{",
-                    RegexCompileFlags.MULTILINE
-                );
-            } catch (RegexError e) {
-                throw new IOError.FAILED(@"Internal error: Failed to compile regex: $(e.message)");
-            }
-
-            if (getter_with_body_regex.match(vala_content)) {
-                throw new IOError.FAILED(@"Error: The markup property in '$(input_path)' already has a getter defined. Expected empty getter: { get; }");
-            }
-
-            // Now check for the empty getter pattern
-            MatchInfo match_info;
-            Regex markup_property_regex;
-            try {
-                // Pattern to match: public override string markup { get; }
-                // Allow for various whitespace patterns
-                markup_property_regex = new Regex(
-                    @"public\\s+override\\s+string\\s+markup\\s*\\{\\s*get\\s*;\\s*\\}",
-                    RegexCompileFlags.MULTILINE
-                );
-            } catch (RegexError e) {
-                throw new IOError.FAILED(@"Internal error: Failed to compile regex: $(e.message)");
-            }
-
-            if (!markup_property_regex.match(vala_content, 0, out match_info)) {
-                throw new IOError.NOT_FOUND(@"Error: No 'public override string markup' property found in '$(input_path)'");
-            }
-
-            // Escape any triple quotes in HTML content by replacing """ with \"\"\"
-            string escaped_html = html_content.replace("\"\"\"", "\\\"\\\"\\\"");
-
-            // Determine the indentation to use for the HTML content
-            // Find the line containing the markup property and extract its indentation
-            string indentation = extract_indentation(vala_content);
-
-            // Generate the replacement - we replace just the "{ get; }" part
-            // Regex to match just the getter part: { get; }
-            Regex getter_regex;
-            try {
-                getter_regex = new Regex(
-                    @"\\{\\s*get\\s*;\\s*\\}",
-                    0
-                );
-            } catch (RegexError e) {
-                throw new IOError.FAILED(@"Internal error: Failed to compile getter regex: $(e.message)");
-            }
-
-            // Build the replacement getter - HTML content placed verbatim between triple quotes
-            string replacement = @"{ get {\n$(indentation)$(indentation)return \"\"\"$(escaped_html)\"\"\";\n$(indentation)}}";
-
-            // Perform the replacement
-            string output_content;
-            try {
-                output_content = getter_regex.replace(vala_content, -1, 0, replacement);
-            } catch (RegexError e) {
-                throw new IOError.FAILED(@"Internal error: Failed to replace markup property: $(e.message)");
-            }
-
-            // Write output file
-            write_file_content(output_path, output_content);
-        }
-
-        private string read_file_content(string path) throws Error {
-            var file = File.new_for_path(path);
-            var input_stream = new DataInputStream(file.read());
-            var builder = new StringBuilder();
-
-            string line;
-            while ((line = input_stream.read_line(null)) != null) {
-                builder.append(line);
-                builder.append("\n");
-            }
-            input_stream.close();
-
-            return builder.str;
-        }
-
-        private void write_file_content(string path, string content) throws Error {
-            var file = File.new_for_path(path);
-            
-            // Ensure parent directory exists
-            var parent = file.get_parent();
-            if (parent != null && !parent.query_exists()) {
-                parent.make_directory_with_parents();
-            }
-
-            var output_stream = new DataOutputStream(file.replace(null, false, FileCreateFlags.NONE));
-            output_stream.put_string(content);
-            output_stream.close();
-        }
-
-        private string extract_indentation(string content) {
-            // Find the line containing "public override string markup" and extract its leading whitespace
-            Regex indent_regex;
-            try {
-                indent_regex = new Regex(
-                    @"^([ \\t]*)public\\s+override\\s+string\\s+markup",
-                    RegexCompileFlags.MULTILINE
-                );
-            } catch (RegexError e) {
-                return "    "; // Default to 4 spaces
-            }
-
-            MatchInfo match;
-            if (indent_regex.match(content, 0, out match)) {
-                return match.fetch(1) ?? "    ";
-            }
-
-            return "    "; // Default to 4 spaces
-        }
-    }
-}

+ 9 - 0
tools/spry-mkconst/meson.build

@@ -0,0 +1,9 @@
+spry_mkconst_sources = files(
+    'spry-mkconst.vala'
+)
+
+spry_mkconst = executable('spry-mkconst',
+    spry_mkconst_sources,
+    dependencies: [glib_dep, gobject_dep, gio_dep],
+    install: true
+)

+ 219 - 0
tools/spry-mkconst/spry-mkconst.vala

@@ -0,0 +1,219 @@
+namespace Spry.Tools {
+
+    public class Mkconst : Object {
+
+        private static bool show_version = false;
+        private static string? output_file = null;
+        private static string? namespace_name = null;
+        private static string? const_name_override = null;
+
+        private const OptionEntry[] options = {
+            { "output", 'o', 0, OptionArg.FILENAME, ref output_file, "Output file path (default: stdout)", "FILE" },
+            { "ns", '\0', 0, OptionArg.STRING, ref namespace_name, "Wrap const in namespace", "NAMESPACE" },
+            { "name", 'n', 0, OptionArg.STRING, ref const_name_override, "Override const name (default: derived from filename)", "NAME" },
+            { "version", 'v', 0, OptionArg.NONE, ref show_version, "Show version information", null },
+            { null }
+        };
+
+        public static int main(string[] args) {
+            try {
+                var opt_context = new OptionContext("<INPUT_FILE> - Convert file content to a Vala string constant");
+                opt_context.set_help_enabled(true);
+                opt_context.add_main_entries(options, null);
+                opt_context.set_description(
+                    "Arguments:\n" +
+                    "  INPUT_FILE         Input file (typically an HTML template file)\n" +
+                    "\n" +
+                    "The const name is derived from the input filename by converting to CAPITAL_SNAKE_CASE.\n" +
+                    "For example, 'UserContentComponent.vala.html' becomes USER_CONTENT_COMPONENT_VALA_HTML.\n" +
+                    "\n" +
+                    "Output goes to stdout by default, or use -o to specify an output file."
+                );
+                opt_context.parse(ref args);
+            } catch (OptionError e) {
+                stderr.printf("Error: %s\n", e.message);
+                stderr.printf("Run '%s --help' for more information.\n", args[0]);
+                return 1;
+            }
+
+            if (show_version) {
+                stdout.printf("spry-mkconst 0.1\n");
+                return 0;
+            }
+
+            // Get input file from remaining arguments
+            string? input_file = null;
+            if (args.length > 1) {
+                input_file = args[1];
+            }
+
+            if (input_file == null) {
+                stderr.printf("Error: No input file specified.\n");
+                stderr.printf("Run '%s --help' for more information.\n", args[0]);
+                return 1;
+            }
+
+            try {
+                var tool = new Mkconst();
+                tool.process(input_file, output_file, namespace_name, const_name_override);
+                return 0;
+            } catch (Error e) {
+                stderr.printf("%s\n", e.message);
+                return 1;
+            }
+        }
+
+        public void process(string input_path, string? output_path, string? ns_name, string? const_name) throws Error {
+            // Check if input file exists
+            var input_file = File.new_for_path(input_path);
+            if (!input_file.query_exists()) {
+                throw new IOError.NOT_FOUND(@"Error: Input file '$(input_path)' does not exist");
+            }
+
+            // Read input file
+            string content = read_file_content(input_path);
+
+            // Normalize line endings to Unix style
+            content = content.replace("\r\n", "\n").replace("\r", "\n");
+
+            // Derive const name from filename if not provided
+            string actual_const_name;
+            if (const_name != null) {
+                actual_const_name = const_name;
+            } else {
+                actual_const_name = filename_to_const_name(input_path);
+            }
+
+            // Escape any triple quotes in content by replacing """ with \"\"\"
+            string escaped_content = content.replace("\"\"\"", "\\\"\\\"\\\"");
+
+            // Determine indentation based on namespace
+            string indent = ns_name != null ? "    " : "";
+
+            // Build output
+            var output = new StringBuilder();
+
+            // Write namespace if provided
+            if (ns_name != null) {
+                output.append(@"namespace $(ns_name) {\n\n");
+            }
+
+            // Write the const declaration
+            // Remove trailing newline from content before wrapping in triple quotes
+            string trimmed_content = escaped_content;
+            if (trimmed_content.has_suffix("\n")) {
+                trimmed_content = trimmed_content.substring(0, trimmed_content.length - 1);
+            }
+            output.append(@"$(indent)const string $(actual_const_name) = \"\"\"$(trimmed_content)\"\"\";\n");
+
+            // Close namespace if provided
+            if (ns_name != null) {
+                output.append("}\n");
+            }
+
+            // Write output
+            if (output_path != null) {
+                write_file_content(output_path, output.str);
+            } else {
+                stdout.printf("%s", output.str);
+            }
+        }
+
+        private string filename_to_const_name(string filepath) {
+            // Get just the filename (without directory path)
+            string basename = Path.get_basename(filepath);
+
+            // Convert to CAPITAL_SNAKE_CASE
+            // Assumes input is PascalCase with possible extensions
+            // e.g., "UserContentComponent.vala.html" -> "USER_CONTENT_COMPONENT_VALA_HTML"
+            var result = new StringBuilder();
+            var prev_was_lower = false;
+            var prev_was_upper = false;
+
+            foreach (var c in basename.to_utf8()) {
+                if (c == '.') {
+                    // Convert dots to underscores
+                    result.append("_");
+                    prev_was_lower = false;
+                    prev_was_upper = false;
+                } else if (c == '-') {
+                    // Convert dashes to underscores
+                    result.append("_");
+                    prev_was_lower = false;
+                    prev_was_upper = false;
+                } else if (c == '_') {
+                    result.append("_");
+                    prev_was_lower = false;
+                    prev_was_upper = false;
+                } else if (c.isupper()) {
+                    if (prev_was_lower) {
+                        // Transition from lower to upper, insert underscore
+                        result.append("_");
+                    } else if (result.len > 0 && prev_was_upper) {
+                        // Check if this is end of an acronym (next char would be lower)
+                        // We'll handle this in the next iteration
+                    }
+                    result.append(c.to_string());
+                    prev_was_lower = false;
+                    prev_was_upper = true;
+                } else if (c.islower()) {
+                    if (prev_was_upper && result.len > 1) {
+                        // Check if previous char was part of an acronym
+                        // e.g., "HTMLFile" -> we want "HTML_FILE"
+                        // Look back to see if we have multiple uppercase before this
+                        string current = result.str;
+                        if (current.length >= 2) {
+                            char prev_char = (char)current.get(current.length - 1);
+                            char prev_prev_char = (char)current.get(current.length - 2);
+                            if (prev_char.isupper() && prev_prev_char.isupper()) {
+                                // Insert underscore before the last uppercase char
+                                result.erase(result.len - 1);
+                                result.append("_");
+                                result.append(prev_char.to_string());
+                            }
+                        }
+                    }
+                    result.append(c.toupper().to_string());
+                    prev_was_lower = true;
+                    prev_was_upper = false;
+                } else if (c.isdigit()) {
+                    result.append(c.to_string());
+                    prev_was_lower = false;
+                    prev_was_upper = false;
+                }
+                // Ignore other characters
+            }
+
+            return result.str;
+        }
+
+        private string read_file_content(string path) throws Error {
+            var file = File.new_for_path(path);
+            var input_stream = new DataInputStream(file.read());
+            var builder = new StringBuilder();
+
+            string line;
+            while ((line = input_stream.read_line(null)) != null) {
+                builder.append(line);
+                builder.append("\n");
+            }
+            input_stream.close();
+
+            return builder.str;
+        }
+
+        private void write_file_content(string path, string content) throws Error {
+            var file = File.new_for_path(path);
+            
+            // Ensure parent directory exists
+            var parent = file.get_parent();
+            if (parent != null && !parent.query_exists()) {
+                parent.make_directory_with_parents();
+            }
+
+            var output_stream = new DataOutputStream(file.replace(null, false, FileCreateFlags.NONE));
+            output_stream.put_string(content);
+            output_stream.close();
+        }
+    }
+}