Skip to content

Commit a72f7e2

Browse files
raifdmuellerclaude
andcommitted
feat: add AsciiDoc ifdef/ifndef conditional block support (#14)
Implement ifdef::attr[]/endif::[] and ifndef::attr[]/endif::[] conditional blocks in the AsciiDoc parser. Supports block form, single-line form (ifdef::attr[inline content]), and nested conditions with proper depth tracking. Conditional filtering runs before include expansion, so ifdef can control whether include directives are processed. Attributes defined via :attr: value are tracked and available for condition evaluation. Includes 25 new tests covering all conditional block scenarios. Bump version to 0.4.27. Fixes #14 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 38076da commit a72f7e2

File tree

6 files changed

+670
-17
lines changed

6 files changed

+670
-17
lines changed

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "dacli"
3-
version = "0.4.26"
3+
version = "0.4.27"
44
description = "Documentation Access CLI - Navigate and query large documentation projects"
55
readme = "README.md"
66
license = { text = "MIT" }

src/dacli/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,4 @@
55
"""
66

77

8-
__version__ = "0.4.26"
8+
__version__ = "0.4.27"

src/dacli/asciidoc_parser.py

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,14 @@
5050
IMAGE_PATTERN = re.compile(r"^image::(.+?)\[(.*)?\]$")
5151
ADMONITION_PATTERN = re.compile(r"^(NOTE|TIP|IMPORTANT|WARNING|CAUTION):\s*(.*)$")
5252

53+
# Conditional block patterns (Issue #14)
54+
# ifdef::attr[] or ifdef::attr[inline content]
55+
IFDEF_PATTERN = re.compile(r"^ifdef::([a-zA-Z0-9_-]+)\[(.*)\]$")
56+
# ifndef::attr[] or ifndef::attr[inline content]
57+
IFNDEF_PATTERN = re.compile(r"^ifndef::([a-zA-Z0-9_-]+)\[(.*)\]$")
58+
# endif::[] or endif::attr[]
59+
ENDIF_PATTERN = re.compile(r"^endif::([a-zA-Z0-9_-]*)?\[\]$")
60+
5361
# Cross-reference pattern: <<target>> or <<target,display text>>
5462
XREF_PATTERN = re.compile(r"<<([^,>]+)(?:,([^>]+))?>>", re.MULTILINE)
5563

@@ -266,6 +274,10 @@ def parse_file(
266274
# Parse attributes first (they can be used in sections)
267275
attributes = self._parse_attributes(lines)
268276

277+
# Filter conditional blocks (ifdef/ifndef/endif) before include expansion
278+
# so that ifdef can control whether includes are processed (Issue #14)
279+
lines = self._filter_conditionals(lines, attributes)
280+
269281
# Expand includes and collect include info
270282
expanded_lines, includes = self._expand_includes(
271283
lines, file_path, _depth, current_chain
@@ -401,6 +413,103 @@ def _parse_attributes(self, lines: list[str]) -> dict[str, str]:
401413

402414
return attributes
403415

416+
def _filter_conditionals(
417+
self, lines: list[str], attributes: dict[str, str]
418+
) -> list[str]:
419+
"""Filter lines based on ifdef/ifndef/endif conditional blocks (Issue #14).
420+
421+
Processes conditional directives and returns only the lines that should
422+
be included based on the current attribute definitions.
423+
424+
Supports:
425+
- ifdef::attr[] / endif::[] - include when attribute is defined
426+
- ifndef::attr[] / endif::[] - include when attribute is NOT defined
427+
- Single-line form: ifdef::attr[content] / ifndef::attr[content]
428+
- Nested conditions with proper depth tracking
429+
430+
Args:
431+
lines: Raw document lines
432+
attributes: Known document attributes
433+
434+
Returns:
435+
Filtered list of lines with conditional directives removed
436+
"""
437+
result: list[str] = []
438+
# Stack of booleans: True = currently including content
439+
# When empty, we're at top level (always including)
440+
condition_stack: list[bool] = []
441+
442+
for line in lines:
443+
stripped = line.strip()
444+
445+
# Check for ifdef::attr[] or ifdef::attr[inline content]
446+
ifdef_match = IFDEF_PATTERN.match(stripped)
447+
if ifdef_match:
448+
attr_name = ifdef_match.group(1)
449+
inline_content = ifdef_match.group(2)
450+
condition_met = attr_name in attributes
451+
452+
if inline_content:
453+
# Single-line form: ifdef::attr[content]
454+
if self._is_including(condition_stack) and condition_met:
455+
attr_match = ATTRIBUTE_PATTERN.match(inline_content)
456+
if attr_match:
457+
attributes[attr_match.group(1)] = attr_match.group(2).strip()
458+
result.append(inline_content)
459+
else:
460+
# Block form: push condition onto stack
461+
currently_including = self._is_including(condition_stack)
462+
condition_stack.append(currently_including and condition_met)
463+
continue
464+
465+
# Check for ifndef::attr[] or ifndef::attr[inline content]
466+
ifndef_match = IFNDEF_PATTERN.match(stripped)
467+
if ifndef_match:
468+
attr_name = ifndef_match.group(1)
469+
inline_content = ifndef_match.group(2)
470+
condition_met = attr_name not in attributes
471+
472+
if inline_content:
473+
# Single-line form: ifndef::attr[content]
474+
if self._is_including(condition_stack) and condition_met:
475+
attr_match = ATTRIBUTE_PATTERN.match(inline_content)
476+
if attr_match:
477+
attributes[attr_match.group(1)] = attr_match.group(2).strip()
478+
result.append(inline_content)
479+
else:
480+
# Block form: push condition onto stack
481+
currently_including = self._is_including(condition_stack)
482+
condition_stack.append(currently_including and condition_met)
483+
continue
484+
485+
# Check for endif::[] or endif::attr[]
486+
endif_match = ENDIF_PATTERN.match(stripped)
487+
if endif_match:
488+
if condition_stack:
489+
condition_stack.pop()
490+
continue
491+
492+
# Track attribute definitions inside conditional blocks
493+
if self._is_including(condition_stack):
494+
attr_match = ATTRIBUTE_PATTERN.match(line)
495+
if attr_match:
496+
attributes[attr_match.group(1)] = attr_match.group(2).strip()
497+
result.append(line)
498+
499+
return result
500+
501+
@staticmethod
502+
def _is_including(condition_stack: list[bool]) -> bool:
503+
"""Check if we're currently in an including state.
504+
505+
Args:
506+
condition_stack: Stack of condition states
507+
508+
Returns:
509+
True if all conditions in the stack are True (or stack is empty)
510+
"""
511+
return not condition_stack or condition_stack[-1]
512+
404513
def _substitute_attributes(self, text: str, attributes: dict[str, str]) -> str:
405514
"""Substitute attribute references in text.
406515

src/docs/spec/05_asciidoc_parser.adoc

Lines changed: 5 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -33,25 +33,16 @@ The parser is **not** a complete Asciidoctor renderer. It:
3333
* Does not parse inline formatting (bold, italic, monospace)
3434
* Does not analyze table contents in detail
3535
* Does not process complex list structures
36-
* Does **not** support conditional blocks (`ifdef::`, `ifndef::`, `ifeval::`)
36+
* Does **not** support `ifeval::[]` conditional evaluation
3737

3838
== Technical Debt
3939

4040
[WARNING]
4141
====
42-
**TD-ADOC-001: Conditional Blocks Not Supported**
42+
**TD-ADOC-001: ifeval Conditional Not Supported**
4343
44-
The following AsciiDoc features are **not** supported in this version:
45-
46-
* `ifdef::attr[]` / `endif::[]`
47-
* `ifndef::attr[]` / `endif::[]`
48-
* `ifeval::[]`
49-
50-
These features are relevant for complex documentation projects with conditional output. A later implementation should:
51-
52-
1. Evaluate attribute-based conditions
53-
2. Be able to show/hide conditional blocks
54-
3. Handle nested conditions
44+
The `ifeval::[]` directive is **not** supported in this version.
45+
`ifdef::attr[]` / `ifndef::attr[]` / `endif::[]` are fully supported (Issue #14).
5546
5647
**Priority:** Low (not required for MVP)
5748
====
@@ -134,7 +125,7 @@ These features are relevant for complex documentation projects with conditional
134125

135126
=== Not Supported
136127

137-
* Conditional blocks (`ifdef::`, `ifndef::`, `ifeval::`) - see Technical Debt
128+
* `ifeval::[]` conditional evaluation - see Technical Debt
138129
* Inline formatting (`*bold*`, `_italic_`, `+mono+`)
139130
* Footnotes
140131
* Bibliography

0 commit comments

Comments
 (0)