# Void Element Implementation Plan

## Overview

This document outlines the implementation plan for adding void elements to wasot. Void elements are self-closing metalines that cannot have children, similar to HTML void elements like `<br>`, `<img>`, etc.

## Syntax Specification

**Regular block** (can have children):
```wasot
.paragraph
content here
```

**Void element** (self-closing, no children):
```wasot
.hr:
.pagebreak:
```

**Void element with full metaline features**:
```wasot
.image: alt text here .. src="photo.jpg" width=800 :img123
```

The trailing `:` after the type name marks the element as void. All other metaline parsing remains the same:
- Inline text (after the type name, before `..`)
- Positional arguments
- Keyword arguments
- ID (`:id`)

## Current Architecture

```
Emitter (WasotFileEmitter)
    │
    ├─ process_line_with_events()
    │      │
    │      └─ detects metalines, emits events
    │
    └─ Events: BlockStarted, BlockFinished, TextFinished, ContextLine
                    │
                    └─ Consumers handle events
```

## Implementation Strategy

### Option A: New `VoidElement` Event (Recommended)

Add a new event type specifically for void elements. This is cleaner and makes the intent explicit.

### Option B: Flag on MetaLine

Add an `is_void` flag to MetaLine, and have consumers check this flag when handling `BlockStarted`. Simpler but less explicit.

**Recommendation**: Option A (new event) - it follows the existing pattern where different element types have different events (`BlockStarted`, `ContextLine`).

---

## Files Requiring Changes

### 1. `src/parsers/wasot/parser.rs`

**Changes needed:**

#### a) Add `is_void` field to `MetaLine` struct (line ~102)
```rust
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct MetaLine {
    pub type_name: String,
    pub inline_text: QuotedString,
    pub level: usize,
    pub id: Id,
    pub line_index: Option<usize>,
    pub arguments: super::arguments::ParsedArguments,
    pub is_void: bool,  // NEW: marks this as a void element
}
```

#### b) Modify `extract_line_type()` function (line ~548)
Detect and strip trailing `:` from type name:
```rust
pub fn extract_line_type(text: &str) -> (String, String, bool) {
    // ... existing logic ...
    // Check if type_name ends with ':'
    let is_void = type_name.ends_with(':');
    let type_name = if is_void {
        type_name.trim_end_matches(':').to_string()
    } else {
        type_name
    };
    (type_name, remaining, is_void)
}
```

#### c) Update `MetaLine::parse()` function (line ~262)
Pass the `is_void` flag through to the MetaLine struct:
```rust
Some(MetaLine {
    type_name,
    inline_text: QuotedString::new(...),
    level,
    id: Id::from(id),
    line_index: Some(line_index),
    arguments,
    is_void,  // NEW
})
```

#### d) Update all MetaLine constructors in tests
Add `is_void: false` to all existing test cases.

---

### 2. `src/events/events.rs`

**Changes needed:**

#### a) Add new `VoidElement` event variant (line ~56)
```rust
pub enum Event {
    BlockStarted { meta_line: MetaLine },
    BlockFinished,
    TextFinished { lines: Vec<String>, links: Vec<Vec<Link>>, footnotes: Vec<Footnote> },
    ContextLine { meta_line: MetaLine },
    VoidElement { meta_line: MetaLine },  // NEW
}
```

#### b) Update `Event::validate()` method (line ~83)
Add validation for the new event type:
```rust
Event::VoidElement { meta_line } => {
    debug_assert!(meta_line.level <= 100, "Void element level should be reasonable");
    debug_assert!(meta_line.is_void, "VoidElement event should have is_void=true");
    true
}
```

#### c) Update tests to include VoidElement cases

---

### 3. `src/emitters/wasot_file_emitter.rs`

**Changes needed:**

#### a) Modify `process_line_with_events()` (line ~134)
After parsing the metaline, check `is_void` and emit appropriate event:
```rust
if let Some(mut meta_line) = MetaLine::parse(line, line_index) {
    // ... existing ID auto-generation logic ...

    if meta_line.is_void {
        // Void element - emit VoidElement event (no BlockFinished needed)
        let event = Event::VoidElement { meta_line };
        if let Ok(mut bus) = self.bus.lock() {
            bus.broadcast(event);
        }
    } else if meta_line.type_name.is_empty() {
        // Heading (self-closing but not void)
        // ... existing heading logic ...
    } else {
        // Regular block
        // ... existing block logic ...
    }
}
```

**Note**: Void elements should NOT be pushed onto the element parser's block stack since they don't have children to close.

---

### 4. `src/parsers/wasot/block.rs`

**Changes needed:**

#### a) Add `VoidElement` variant to `Element` enum (line ~6)
```rust
pub enum Element {
    Block(Block),
    Text(Text),
    ContextLine(MetaLine),
    VoidElement(MetaLine),  // NEW
}
```

#### b) Update `Element::is_empty()` method (line ~86)
```rust
pub fn is_empty(&self) -> bool {
    match self {
        Element::Block(block) => block.is_empty(),
        Element::Text(text) => text.is_empty(),
        Element::ContextLine(_) => false,
        Element::VoidElement(_) => false,  // NEW: void elements are never empty
    }
}
```

#### c) Add `as_void_element()` accessor method
```rust
pub fn as_void_element(&self) -> Option<&MetaLine> {
    match self {
        Element::VoidElement(meta_line) => Some(meta_line),
        _ => None,
    }
}
```

---

### 5. `src/consumers/wasot_reconstructor.rs`

**Changes needed:**

#### a) Handle `VoidElement` event in `process_event()` (line ~253)
```rust
Event::VoidElement { meta_line } => {
    self.just_closed = false;

    // Ensure document ID is written before first content
    if let Err(e) = self.ensure_document_id() {
        eprintln!("Error writing document ID: {e}");
        return;
    }

    // Write the void element meta-line with trailing ':'
    let line = self.format_void_meta_line(meta_line);
    if let Err(e) = writeln!(self.writer, "{line}") {
        eprintln!("Error writing void element: {e}");
    }
}
```

#### b) Add `format_void_meta_line()` method
Similar to `format_meta_line()` but adds trailing `:` after type name:
```rust
fn format_void_meta_line(&self, meta_line: &MetaLine) -> String {
    let mut line = String::with_capacity(128);

    // Add dots for level
    for _ in 0..meta_line.level {
        line.push('.');
    }

    // Add type name with trailing ':'
    if meta_line.type_name.contains(' ') {
        line.push('"');
        line.push_str(&meta_line.type_name);
        line.push('"');
    } else {
        line.push_str(&meta_line.type_name);
    }
    line.push(':');  // Mark as void

    // ... rest same as format_meta_line() for inline_text, args, kwargs, id ...
    line
}
```

---

### 6. `src/consumers/to_xml_consumer.rs`

**Changes needed:**

#### a) Handle `VoidElement` event in `process_event()` (line ~339)
```rust
Event::VoidElement { meta_line } => {
    // Create a self-closing block element
    let block = Block::new(meta_line.clone());
    if let Err(e) = self.write_void_element(&block) {
        eprintln!("XML conversion error on void element: {e}");
    }
}
```

#### b) Add `write_void_element()` method
Write a self-closing XML element:
```rust
fn write_void_element(&mut self, block: &Block) -> Result<(), xml::writer::Error> {
    // Use self-closing tag: <block type="hr" level="1" void="true" />
    // ... similar to start_element but writes a self-closing tag ...
}
```

---

### 7. `src/consumers/to_jsonl_consumer.rs`

**Changes needed:**

#### a) Handle `VoidElement` event in `process_event()` (line ~40)
```rust
Event::VoidElement { meta_line } => {
    // Void elements at level 1 are written directly as root elements
    if meta_line.level == 1 {
        let block = Block::new(meta_line.clone());
        if let Err(e) = self.write_block(&block) {
            eprintln!("Error writing void element to JSON: {e}");
        }
    }
    // Nested void elements can be ignored for JSONL (root-only output)
}
```

---

### 8. `src/parsers/wasot/element_parser.rs`

**Changes needed:**

Review the `ElementParser` to ensure void elements don't get pushed onto the block stack since they have no children and need no closing.

---

### 9. Other Consumers

Any other consumers (SQLite, selector consumer, etc.) need to handle the new `VoidElement` event. Check:
- `src/consumers/to_sqlite_consumer.rs`
- `src/consumers/selector_consumer.rs`
- Any demo or example consumers

---

## Test Cases to Add

### Parser Tests
1. Basic void element: `.hr:`
2. Void element with inline text: `.img: alt text`
3. Void element with args: `.img: .. src="photo.jpg"`
4. Void element with kwargs: `.hr: .. style=dashed`
5. Void element with ID: `.img: :img1`
6. Full void element: `.img: alt text .. src="x.jpg" width=100 :img1`
7. Nested void element: `..br:`
8. Quoted type void element: `."custom type":`

### Reconstructor Tests
1. Round-trip void element preserves `:` suffix
2. Void elements don't interfere with block nesting

### XML Consumer Tests
1. Void element produces self-closing tag
2. Void element has `void="true"` attribute

### JSONL Tests
1. Root-level void element is written
2. Nested void element is handled correctly

---

## Implementation Order

1. **Phase 1: Parser** (no breaking changes)
   - Add `is_void` field to `MetaLine` (default false)
   - Modify `extract_line_type()` to detect `:`
   - Update `MetaLine::parse()` to pass through `is_void`

2. **Phase 2: Events**
   - Add `VoidElement` event type
   - Update `Event::validate()`

3. **Phase 3: Emitter**
   - Update `process_line_with_events()` to emit `VoidElement`

4. **Phase 4: Block/Element**
   - Add `VoidElement` variant to `Element` enum

5. **Phase 5: Consumers**
   - Update `wasot_reconstructor.rs`
   - Update `to_xml_consumer.rs`
   - Update `to_jsonl_consumer.rs`
   - Update any other consumers

6. **Phase 6: Tests**
   - Add comprehensive tests for all phases

---

## Backward Compatibility

- Existing wasot files without void elements will continue to work unchanged
- The `is_void` field defaults to `false`
- Consumers that don't handle `VoidElement` event will simply ignore them (graceful degradation)

---

## Open Questions

1. **Should void elements participate in auto-ID generation?**
   - Recommendation: Yes, they can have IDs just like regular blocks

2. **Can void elements appear at any nesting level?**
   - Recommendation: Yes, `..br:` is a valid level-2 void element

3. **Should void elements affect the block stack for auto-closing?**
   - Recommendation: No, void elements don't open a scope to close

4. **What about the heading special case?**
   - Headings (empty type name, e.g., `..`) are already self-closing but are NOT void elements
   - Headings use a different mechanism (emit BlockStarted + immediate BlockFinished)
   - Void elements should be handled separately via the VoidElement event
