Coverage for little_loops/frontmatter.py: 17%

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

2

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

4 issue_parser, sync, and issue_history modules.

5"""

6

7 from __future__ import annotations

8

9 import logging

10 import re

11 from typing import Any

12

13 logger = logging.getLogger(__name__)

14

15

16 def 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 only. Lists,

21 block scalars, and nested structures are not supported and will emit

22 a ``logging.WARNING``. Returns empty dict if no frontmatter found.

23

24 Args:

25 content: File content to parse

26 coerce_types: If True, coerce digit strings to int

27

28 Returns:

29 Dictionary of frontmatter fields, or empty dict

30 """

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

32 return {}

33

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

35 if not end_match:

36 return {}

37

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

39

40 result: dict[str, Any] = {}

41 for line in frontmatter_text.split("\n"):

42 line = line.strip()

43 if not line or line.startswith("#"):

44 continue

45 if line.startswith("- "):

46 logger.warning("Unsupported YAML list syntax in frontmatter: %r", line)

47 continue

48 if ":" in line:

49 key, value = line.split(":", 1)

50 key = key.strip()

51 value = value.strip()

52 if value.startswith("|") or value.startswith(">"):

53 logger.warning("Unsupported YAML block scalar in frontmatter: %r", line)

54 result[key] = None

55 continue

56 if value.lower() in ("null", "~", ""):

57 result[key] = None

58 elif coerce_types and value.isdigit():

59 result[key] = int(value)

60 else:

61 result[key] = value

62 return result

63

64

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

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

67

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

69 returns everything after the closing delimiter.

70

71 Args:

72 content: File content possibly starting with frontmatter

73

74 Returns:

75 Content with frontmatter removed. Returns original content

76 unchanged if no valid frontmatter block is found.

77 """

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

79 return content

80

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

82 if not end_match:

83 return content

84

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