Ootempl.Block (ootempl v0.3.0)
Detects and parses block markers for hierarchical list iteration in tables.
Block markers allow creating nested table structures where rows are repeated for each item in a list, with support for header/body/footer sections.
Markers follow the syntax:
{{#list_key}}- Start iteration over a list{{/list_key}}- End iteration block
Table Structure Example
Block markers are placed in dedicated rows that will be removed from output:
| {{#revcode_data}} | | | <- Marker row (removed)
| {{revcode}} | {{desc}} | {{amount}} | <- Header row
| {{#children}} | | | <- Marker row (removed)
| | {{desc}} | {{cost}} | <- Body row (repeated)
| {{/children}} | | | <- Marker row (removed)
| | Subtotal: | {{subtotal}}| <- Footer row
| {{/revcode_data}} | | | <- Marker row (removed)Data Scoping
- Header/footer rows access parent item fields directly
- Body rows access both parent and child fields
- Child fields take precedence on name conflicts
Examples
iex> Ootempl.Block.detect_markers("{{#items}} content {{/items}}")
[
%{type: :open, list_key: "items", position: 0},
%{type: :close, list_key: "items", position: 19}
]
iex> Ootempl.Block.contains_markers?("{{#items}}")
true
iex> Ootempl.Block.contains_markers?("{{name}}")
false
Summary
Functions
Checks if the given text contains any block markers.
Detects all block markers in the given text.
Expands a block structure with the provided data.
Returns all row indices that should be removed from the original table.
Parses the table structure to identify block boundaries and row categories.
Validates that all block markers are properly paired.
Types
@type block_structure() :: %{ list_key: String.t(), open_row_index: non_neg_integer(), close_row_index: non_neg_integer(), header_rows: [non_neg_integer()], body_block: block_structure() | nil, footer_rows: [non_neg_integer()] }
Functions
Checks if the given text contains any block markers.
This is a quick check to determine if block processing is needed.
Parameters
text- The text to check
Returns
true if the text contains {{#...}} or {{/...}} markers, false otherwise.
Examples
iex> Ootempl.Block.contains_markers?("{{#items}}")
true
iex> Ootempl.Block.contains_markers?("{{name}}")
false
iex> Ootempl.Block.contains_markers?("plain text")
false
@spec detect_markers(String.t()) :: [block_marker()]
Detects all block markers in the given text.
Returns a list of block markers in order of appearance with their positions.
Parameters
text- The text to scan for block markers
Returns
A list of marker maps, each containing:
:type- Either:openor:close:list_key- The list variable name:position- Character position in the text
Examples
iex> Ootempl.Block.detect_markers("{{#items}}content{{/items}}")
[
%{type: :open, list_key: "items", position: 0},
%{type: :close, list_key: "items", position: 17}
]
iex> Ootempl.Block.detect_markers("no markers here")
[]
@spec expand_block(block_structure(), [Ootempl.Xml.xml_element()], map()) :: [ {Ootempl.Xml.xml_element(), map()} ]
Expands a block structure with the provided data.
For each item in the list:
- Clone header rows with parent item data
- If there's a nested body_block, recursively expand it
- Clone footer rows with parent item data
Marker-only rows are excluded from the output.
Parameters
structure- The block structure fromparse_table_structure/2rows- The original table rowsdata- The data map containing the list to iterate
Returns
A list of {row_element, scoped_data} tuples ready for placeholder replacement.
@spec marker_row_indices(block_structure()) :: [non_neg_integer()]
Returns all row indices that should be removed from the original table.
These are the marker-only rows that contain block markers but no data placeholders.
@spec parse_table_structure([Ootempl.Xml.xml_element()], map()) :: {:ok, block_structure()} | {:error, term()}
Parses the table structure to identify block boundaries and row categories.
Analyzes table rows to build a block structure that identifies:
- Which rows are marker-only rows (to be removed)
- Which rows are header rows (before nested block)
- Which rows form the body block (nested iteration)
- Which rows are footer rows (after nested block)
Parameters
rows- List of table row XML elementsdata- The data map to validate list keys against
Returns
{:ok, block_structure}with the parsed structure{:error, reason}if parsing fails
Examples
# Single-level block
{:ok, structure} = Block.parse_table_structure(rows, data)
# => %{
# list_key: "items",
# open_row_index: 0,
# close_row_index: 2,
# header_rows: [1],
# body_block: nil,
# footer_rows: []
# }
@spec validate_pairs([block_marker()]) :: :ok | {:error, String.t()}
Validates that all block markers are properly paired.
Uses stack-based validation to ensure each {{#key}} has a corresponding
{{/key}} and detects orphaned or mismatched markers.
Parameters
markers- List of block markers fromdetect_markers/1
Returns
:okif all markers are properly paired{:error, reason}if validation fails
Examples
iex> Ootempl.Block.validate_pairs([
...> %{type: :open, list_key: "items", position: 0},
...> %{type: :close, list_key: "items", position: 10}
...> ])
:ok
iex> Ootempl.Block.validate_pairs([
...> %{type: :open, list_key: "items", position: 0}
...> ])
{:error, "Unmatched {{#items}} at position 0"}
iex> Ootempl.Block.validate_pairs([
...> %{type: :close, list_key: "items", position: 0}
...> ])
{:error, "Orphan {{/items}} at position 0 (no matching {{#items}})"}
iex> Ootempl.Block.validate_pairs([
...> %{type: :open, list_key: "items", position: 0},
...> %{type: :close, list_key: "other", position: 10}
...> ])
{:error, "Mismatched block: found {{/other}} at position 10, expected {{/items}}"}