Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,9 @@ build/
# Serena/Vibe working directories
.serena/
.vibe/

# Playwright MCP screenshots
.playwright-mcp/

# Test reports
report.html
1 change: 1 addition & 0 deletions src/dacli/api/content.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,7 @@ def get_elements(
start_line=elem.source_location.line,
end_line=elem.source_location.end_line or elem.source_location.line,
),
attributes=elem.attributes, # Issue #159: Include element attributes
)
)

Expand Down
6 changes: 5 additions & 1 deletion src/dacli/api/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
and Content Access API as specified in 02_api_specification.adoc.
"""

from typing import Literal
from typing import Any, Literal

from pydantic import BaseModel, Field

Expand Down Expand Up @@ -123,6 +123,10 @@ class ElementItem(BaseModel):
path: str = Field(description="Section path containing this element")
index: int = Field(description="Index of element within its section")
location: ElementLocation
attributes: dict[str, Any] = Field(
default_factory=dict,
description="Element-specific attributes and content (Issue #159)",
)
# Note: preview field removed in Issue #142 as redundant


Expand Down
71 changes: 64 additions & 7 deletions src/dacli/asciidoc_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -667,6 +667,13 @@ def _parse_elements(
current_list_element: Element | None = None # Track list for end_line (#136)
# Track ALL open blocks for end_line (Issue #157: use stack instead of single element)
open_blocks: list[Element] = []
# Content tracking for Issue #159
code_block_content: list[str] = []
plantuml_content: list[str] = []
mermaid_content: list[str] = []
ditaa_content: list[str] = []
table_content: list[str] = []
list_content: list[str] = []

for line_text, source_file, line_num, resolved_from in lines:
# Track current section for parent_section
Expand Down Expand Up @@ -723,6 +730,7 @@ def _sub_attr(match: re.Match) -> str:
if pending_plantuml_info is not None:
# Start of plantuml block
in_plantuml_block = True
plantuml_content = [] # Initialize content tracking (Issue #159)
name, fmt = pending_plantuml_info
element = self._create_diagram_element(
"plantuml", name, fmt, source_file, line_num,
Expand All @@ -734,6 +742,7 @@ def _sub_attr(match: re.Match) -> str:
elif pending_mermaid_info is not None:
# Start of mermaid block
in_mermaid_block = True
mermaid_content = [] # Initialize content tracking (Issue #159)
name, fmt = pending_mermaid_info
element = self._create_diagram_element(
"mermaid", name, fmt, source_file, line_num,
Expand All @@ -745,6 +754,7 @@ def _sub_attr(match: re.Match) -> str:
elif pending_ditaa_info is not None:
# Start of ditaa block
in_ditaa_block = True
ditaa_content = [] # Initialize content tracking (Issue #159)
name, fmt = pending_ditaa_info
element = self._create_diagram_element(
"ditaa", name, fmt, source_file, line_num,
Expand All @@ -756,6 +766,7 @@ def _sub_attr(match: re.Match) -> str:
elif pending_code_language is not None:
# Start of code block
in_code_block = True
code_block_content = [] # Initialize content tracking (Issue #159)
source_location = SourceLocation(
file=source_file,
line=line_num,
Expand All @@ -771,19 +782,27 @@ def _sub_attr(match: re.Match) -> str:
open_blocks.append(element)
pending_code_language = None
elif in_code_block:
# End of code block
# End of code block - save content (Issue #159)
if open_blocks:
open_blocks[-1].attributes["content"] = "\n".join(code_block_content)
in_code_block = False
self._close_open_block(open_blocks, line_num)
elif in_plantuml_block:
# End of plantuml block
# End of plantuml block - save content (Issue #159)
if open_blocks:
open_blocks[-1].attributes["content"] = "\n".join(plantuml_content)
in_plantuml_block = False
self._close_open_block(open_blocks, line_num)
elif in_mermaid_block:
# End of mermaid block
# End of mermaid block - save content (Issue #159)
if open_blocks:
open_blocks[-1].attributes["content"] = "\n".join(mermaid_content)
in_mermaid_block = False
self._close_open_block(open_blocks, line_num)
elif in_ditaa_block:
# End of ditaa block
# End of ditaa block - save content (Issue #159)
if open_blocks:
open_blocks[-1].attributes["content"] = "\n".join(ditaa_content)
in_ditaa_block = False
self._close_open_block(open_blocks, line_num)
continue
Expand All @@ -793,6 +812,7 @@ def _sub_attr(match: re.Match) -> str:
if not in_table:
# Start of table
in_table = True
table_content = [] # Initialize content tracking (Issue #159)
source_location = SourceLocation(
file=source_file,
line=line_num,
Expand All @@ -807,7 +827,9 @@ def _sub_attr(match: re.Match) -> str:
elements.append(element)
open_blocks.append(element)
else:
# End of table
# End of table - save content (Issue #159)
if open_blocks:
open_blocks[-1].attributes["content"] = "\n".join(table_content)
in_table = False
self._close_open_block(open_blocks, line_num)
continue
Expand Down Expand Up @@ -852,36 +874,52 @@ def _sub_attr(match: re.Match) -> str:
parent_section=current_section_path,
)
)
# Save list content before reset (Issue #159)
if current_list_element is not None and list_content:
current_list_element.attributes["content"] = "\n".join(list_content)
current_list_type = None # Reset list tracking
current_list_element = None
continue

# Detect lists (unordered, ordered, description)
# Check for unordered list (* item)
if UNORDERED_LIST_PATTERN.match(line_text):
if current_list_type != "unordered":
# Save previous list content if any (Issue #159)
if current_list_element is not None and list_content:
current_list_element.attributes["content"] = "\n".join(list_content)
# Start of a new unordered list
current_list_type = "unordered"
list_content = [] # Initialize content tracking (Issue #159)
current_list_element = self._create_list_element(
"unordered", source_file, line_num,
resolved_from, current_section_path,
)
elements.append(current_list_element)
elif current_list_element is not None:
# Collect list item content (Issue #159)
list_content.append(line_text)
if current_list_element is not None:
# Continue list - update end_line (Issue #136)
current_list_element.source_location.end_line = line_num
continue

# Check for ordered list (. item)
if ORDERED_LIST_PATTERN.match(line_text):
if current_list_type != "ordered":
# Save previous list content if any (Issue #159)
if current_list_element is not None and list_content:
current_list_element.attributes["content"] = "\n".join(list_content)
# Start of a new ordered list
current_list_type = "ordered"
list_content = [] # Initialize content tracking (Issue #159)
current_list_element = self._create_list_element(
"ordered", source_file, line_num,
resolved_from, current_section_path,
)
elements.append(current_list_element)
elif current_list_element is not None:
# Collect list item content (Issue #159)
list_content.append(line_text)
if current_list_element is not None:
# Continue list - update end_line (Issue #136)
current_list_element.source_location.end_line = line_num
continue
Expand All @@ -903,9 +941,24 @@ def _sub_attr(match: re.Match) -> str:

# If line is not a list item, reset list tracking
if line_text.strip():
# Save list content before reset (Issue #159)
if current_list_element is not None and list_content:
current_list_element.attributes["content"] = "\n".join(list_content)
current_list_type = None
current_list_element = None

# Collect content for open blocks (Issue #159)
if in_code_block:
code_block_content.append(line_text)
elif in_plantuml_block:
plantuml_content.append(line_text)
elif in_mermaid_block:
mermaid_content.append(line_text)
elif in_ditaa_block:
ditaa_content.append(line_text)
elif in_table:
table_content.append(line_text)

# Handle ALL unclosed blocks - set end_line to last line of their source file
# Issue #146: unclosed code blocks should have proper end_line
# Issue #157: ALL unclosed blocks should be detected, not just the last one
Expand Down Expand Up @@ -938,6 +991,10 @@ def _sub_attr(match: re.Match) -> str:
message=warning_msg,
))

# Save list content if list is still open at end of file (Issue #159)
if current_list_element is not None and list_content:
current_list_element.attributes["content"] = "\n".join(list_content)

return elements, warnings

def _find_section_path(self, sections: list[Section], title: str) -> str:
Expand Down
63 changes: 41 additions & 22 deletions src/dacli/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -480,44 +480,63 @@ def search(ctx: CliContext, query: str, scope: str | None, max_results: int):

@cli.command(epilog="""
Examples:
dacli elements # All elements
dacli elements --type code # Only code blocks
dacli elements --type table # Only tables
dacli elements api # Elements in 'api' section
dacli elements api --recursive # Elements in 'api' and all subsections
dacli --format json el --type image # JSON output using alias
dacli elements # All elements (metadata only)
dacli elements --type code # Only code blocks
dacli elements --include-content # Include element content
dacli elements --include-content --content-limit 10 # First 10 lines only
dacli elements api --recursive # Elements in 'api' and subsections
dacli --format json el --type image # JSON output using alias
""")
@click.argument("section_path", required=False, default=None)
@click.option("--type", "element_type", default=None,
help="Element type: code, table, image, diagram, list")
@click.option("--recursive", is_flag=True, default=False,
help="Include elements from child sections")
@click.option("--include-content", is_flag=True, default=False,
help="Include element content (Issue #159)")
@click.option("--content-limit", type=int, default=None,
help="Limit content to first N lines (requires --include-content)")
@pass_context
def elements(ctx: CliContext, section_path: str | None, element_type: str | None,
recursive: bool):
recursive: bool, include_content: bool, content_limit: int | None):
"""Get elements (code blocks, tables, images) from documentation."""
elems = ctx.index.get_elements(
element_type=element_type,
section_path=section_path,
recursive=recursive,
)

# Note: preview field removed in Issue #142 as redundant
# The type field is sufficient to identify element kind
# Build element dicts with optional content (Issue #159)
element_dicts = []
for e in elems:
elem_dict = {
"type": e.type,
"parent_section": e.parent_section,
"location": {
"file": str(e.source_location.file),
"start_line": e.source_location.line,
"end_line": e.source_location.end_line,
},
}

# Include attributes if requested (Issue #159)
if include_content:
attributes = dict(e.attributes) # Copy attributes

# Apply content limit if specified
if content_limit is not None and "content" in attributes:
content = attributes["content"]
lines = content.split("\n")
if len(lines) > content_limit:
attributes["content"] = "\n".join(lines[:content_limit])

elem_dict["attributes"] = attributes

element_dicts.append(elem_dict)

result = {
"elements": [
{
"type": e.type,
"parent_section": e.parent_section,
"location": {
"file": str(e.source_location.file),
"start_line": e.source_location.line,
"end_line": e.source_location.end_line,
},
}
for e in elems
],
"count": len(elems),
"elements": element_dicts,
"count": len(element_dicts),
}
click.echo(format_output(ctx, result))

Expand Down
Loading
Loading