Skip to content

Commit a76be61

Browse files
committed
feat: implement structured extraction checkpoint B3
Signed-off-by: Abhijeet Saharan <abhijeetsaharan2236@gmail.com>
1 parent dc9d2d0 commit a76be61

2 files changed

Lines changed: 46 additions & 18 deletions

File tree

application/defs/cheatsheet_defs.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ def __post_init__(self):
4343
}
4444

4545
# Validate fields which require string values.
46-
for field_name in required_str_fields:
46+
for field_name in required_str_fields:
4747
value = getattr(self, field_name)
4848

4949
if not isinstance(value, str) or not value:

application/utils/external_project_parsers/parsers/cheatsheet_extractor.py

Lines changed: 45 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1+
import logging
12
import os
23
import re
34

45
from application.defs.cheatsheet_defs import CheatsheetRecord
56

67
PARSER_VERSION = "v1"
7-
FALLBACK_USED = "false"
88

99
CANONICAL_BASE_URL = "https://cheatsheetseries.owasp.org/cheatsheets/"
1010

@@ -41,28 +41,44 @@ def _extract_body_after_heading(markdown: str, heading_match: re.Match) -> str:
4141

4242

4343
def _extract_summary(markdown: str) -> str:
44-
"""Extract a summary section from cheatsheet markdown."""
44+
"""Extract summary from Introduction section in cheatsheet markdown."""
4545

46-
all_heading_matches = list(_ANY_HEADING_RE.finditer(markdown))
47-
48-
for match in all_heading_matches:
49-
heading_text = match.group().lstrip("#").strip()
50-
51-
if heading_text.lower() == "introduction":
46+
for match in _ANY_HEADING_RE.finditer(markdown):
47+
if match.group().lstrip("#").strip().lower() == "introduction":
5248
body = _extract_body_after_heading(markdown, match)
53-
5449
if body:
5550
return body
5651

57-
break
52+
raise ValueError(
53+
"_extract_summary: no suitable summary section could be extracted from markdown."
54+
)
55+
5856

59-
for match in all_heading_matches:
60-
body = _extract_body_after_heading(markdown, match)
57+
def _extract_title(markdown: str) -> str:
58+
"""Extract H1 title from cheatsheet markdown."""
59+
60+
match = _TITLE_RE.search(markdown)
61+
if not match:
62+
raise ValueError("_extract_title: no title found in markdown.")
63+
64+
return match.group("title").strip()
6165

66+
67+
def _fallback_title() -> str:
68+
"""Return fallback title for malformed markdown."""
69+
70+
return "No title found."
71+
72+
73+
def _fallback_summary(markdown: str) -> str:
74+
"""Return first non-empty paragraph after any heading, or 'No summary found.'"""
75+
76+
for match in _ANY_HEADING_RE.finditer(markdown):
77+
body = _extract_body_after_heading(markdown, match)
6278
if body:
6379
return body
6480

65-
raise ValueError("_extract_summary: no summary could be extracted from markdown.")
81+
return "No summary found."
6682

6783

6884
def extract_cheatsheet_record(
@@ -71,12 +87,24 @@ def extract_cheatsheet_record(
7187
) -> CheatsheetRecord:
7288
"""Extract a structured CheatsheetRecord from markdown content."""
7389

74-
title_match = _TITLE_RE.search(markdown)
75-
title = title_match.group("title").strip()
90+
fallback_used = "false"
91+
92+
try:
93+
title = _extract_title(markdown)
94+
except ValueError as e:
95+
logging.warning(str(e))
96+
title = _fallback_title()
97+
fallback_used = "true"
7698

99+
# Headings can be empty.
77100
headings = [m.group("heading").strip() for m in _HEADING_RE.finditer(markdown)]
78101

79-
summary = _extract_summary(markdown)
102+
try:
103+
summary = _extract_summary(markdown)
104+
except ValueError as e:
105+
logging.warning(str(e))
106+
summary = _fallback_summary(markdown)
107+
fallback_used = "true"
80108

81109
source_id = _derive_source_id(source_path)
82110
hyperlink = _derive_hyperlink(source_path)
@@ -91,6 +119,6 @@ def extract_cheatsheet_record(
91119
category_hints=[],
92120
metadata={
93121
"parser_version": PARSER_VERSION,
94-
"fallback_used": FALLBACK_USED,
122+
"fallback_used": fallback_used,
95123
},
96124
)

0 commit comments

Comments
 (0)