Coverage for little_loops / frontmatter.py: 58%
52 statements
« prev ^ index » next coverage.py v7.12.0, created at 2026-04-11 23:20 -0500
« prev ^ index » next coverage.py v7.12.0, created at 2026-04-11 23:20 -0500
1"""Frontmatter parsing utilities for little-loops.
3Provides shared YAML-subset frontmatter parsing and stripping used by
4issue_parser, sync, and issue_history modules.
5"""
7from __future__ import annotations
9import logging
10import re
11from typing import Any
13logger = logging.getLogger(__name__)
16def parse_frontmatter(content: str, *, coerce_types: bool = False) -> dict[str, Any]:
17 """Extract YAML frontmatter from content.
19 Looks for content between opening and closing '---' markers.
20 Parses a subset of YAML: simple ``key: value`` pairs and YAML block
21 sequences (``key:`` followed by ``- item`` lines). Block scalars and
22 nested structures are not supported and will emit a ``logging.WARNING``.
23 Returns empty dict if no frontmatter found.
25 Args:
26 content: File content to parse
27 coerce_types: If True, coerce digit strings to int
29 Returns:
30 Dictionary of frontmatter fields, or empty dict
31 """
32 if not content or not content.startswith("---"):
33 return {}
35 end_match = re.search(r"\n---\s*\n", content[3:])
36 if not end_match:
37 return {}
39 frontmatter_text = content[4 : 3 + end_match.start()]
41 result: dict[str, Any] = {}
42 current_list_key: str | None = None
43 for line in frontmatter_text.split("\n"):
44 line = line.strip()
45 if not line or line.startswith("#"):
46 continue
47 if line.startswith("- "):
48 if current_list_key is not None:
49 result[current_list_key].append(line[2:].strip())
50 else:
51 logger.warning("Unsupported YAML list syntax in frontmatter: %r", line)
52 continue
53 # Non-list line: finalize any in-progress empty list, then reset
54 if current_list_key is not None and result[current_list_key] == []:
55 result[current_list_key] = None
56 current_list_key = None
57 if ":" in line:
58 key, value = line.split(":", 1)
59 key = key.strip()
60 value = value.strip()
61 if value.startswith("|") or value.startswith(">"):
62 logger.warning("Unsupported YAML block scalar in frontmatter: %r", line)
63 result[key] = None
64 continue
65 if value.lower() in ("null", "~", ""):
66 if value == "":
67 result[key] = []
68 current_list_key = key
69 else:
70 result[key] = None
71 elif coerce_types and value.isdigit():
72 result[key] = int(value)
73 else:
74 result[key] = value
75 # Finalize any trailing empty list key
76 if current_list_key is not None and result[current_list_key] == []:
77 result[current_list_key] = None
78 return result
81def strip_frontmatter(content: str) -> str:
82 """Remove YAML frontmatter from content, returning the body.
84 Strips the ``---`` delimited frontmatter block (if present) and
85 returns everything after the closing delimiter.
87 Args:
88 content: File content possibly starting with frontmatter
90 Returns:
91 Content with frontmatter removed. Returns original content
92 unchanged if no valid frontmatter block is found.
93 """
94 if not content or not content.startswith("---"):
95 return content
97 end_match = re.search(r"\n---\s*\n", content[3:])
98 if not end_match:
99 return content
101 return content[3 + end_match.end() :]