spry-mkcomponent-design.md 7.7 KB

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

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

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

<p>You said: <strong sid="message"></strong> <em>(via <span sid="action"></span>)</em></p>

Output: UserContentComponent.vala (generated)

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:

# 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:

# 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

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)