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

1"""Frontmatter parsing utilities for little-loops. 

2 

3Provides shared YAML-subset frontmatter parsing and stripping used by 

4issue_parser, sync, and issue_history modules. 

5""" 

6 

7from __future__ import annotations 

8 

9import logging 

10import re 

11from typing import Any 

12 

13logger = logging.getLogger(__name__) 

14 

15 

16def parse_frontmatter(content: str, *, coerce_types: bool = False) -> dict[str, Any]: 

17 """Extract YAML frontmatter from content. 

18 

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. 

24 

25 Args: 

26 content: File content to parse 

27 coerce_types: If True, coerce digit strings to int 

28 

29 Returns: 

30 Dictionary of frontmatter fields, or empty dict 

31 """ 

32 if not content or not content.startswith("---"): 

33 return {} 

34 

35 end_match = re.search(r"\n---\s*\n", content[3:]) 

36 if not end_match: 

37 return {} 

38 

39 frontmatter_text = content[4 : 3 + end_match.start()] 

40 

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 

79 

80 

81def strip_frontmatter(content: str) -> str: 

82 """Remove YAML frontmatter from content, returning the body. 

83 

84 Strips the ``---`` delimited frontmatter block (if present) and 

85 returns everything after the closing delimiter. 

86 

87 Args: 

88 content: File content possibly starting with frontmatter 

89 

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 

96 

97 end_match = re.search(r"\n---\s*\n", content[3:]) 

98 if not end_match: 

99 return content 

100 

101 return content[3 + end_match.end() :]