diff --git a/frontmatter/default_handlers.py b/frontmatter/default_handlers.py index 8c5d8ec..3a87535 100644 --- a/frontmatter/default_handlers.py +++ b/frontmatter/default_handlers.py @@ -257,7 +257,27 @@ def load(self, fm: str, **kwargs: object) -> Any: Parse YAML front matter. This uses yaml.SafeLoader by default. """ kwargs.setdefault("Loader", SafeLoader) - return yaml.load(fm, **kwargs) # type: ignore[arg-type] + try: + return yaml.load(fm, **kwargs) # type: ignore[arg-type] + except yaml.YAMLError as exc: + mark = getattr(exc, "problem_mark", None) + location = ( + f" at line {mark.line + 1}, column {mark.column + 1}" + if mark is not None + else "" + ) + problem = getattr(exc, "problem", None) or str(exc).splitlines() + if isinstance(problem, list): + problem = problem[0] if problem else "YAML parsing failed" + problem = problem.rstrip(".") + hint = ( + " If a plain-text value contains ': ', quote it." + if problem == "mapping values are not allowed in this context" + else "" + ) + raise yaml.YAMLError( + f"Invalid YAML front matter{location}: {problem}.{hint}" + ) from exc def export(self, metadata: dict[str, object], **kwargs: object) -> str: """ diff --git a/tests/unit_test.py b/tests/unit_test.py index 188fb4c..588ec2c 100644 --- a/tests/unit_test.py +++ b/tests/unit_test.py @@ -1,11 +1,13 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- +import io import os import shutil import tempfile import textwrap import unittest +import yaml import frontmatter from frontmatter.default_handlers import YAMLHandler, JSONHandler, TOMLHandler @@ -312,6 +314,26 @@ def test_split_load(self): class YAMLHandlerTest(HandlerBaseTest, unittest.TestCase): + def test_invalid_colon_value_explains_yaml_error(self): + text = '---\ntitle: Assignment 1: Some Title\n---\n\n# Hello world!' + + with self.assertRaisesRegex( + yaml.YAMLError, "Invalid YAML front matter.*line 2.*quote it" + ) as raised: + frontmatter.load(io.StringIO(text)) + + self.assertIsInstance(raised.exception.__cause__, yaml.YAMLError) + + def test_quoted_colon_value_still_loads(self): + text = '---\ntitle: "Assignment 1: Some Title"\n---\n\n# Hello world!' + self.assertEqual(frontmatter.loads(text)["title"], "Assignment 1: Some Title") + + def test_other_yaml_errors_get_context_without_colon_hint(self): + text = "---\ntitle: [unfinished\n---\n\n# Hello world!" + with self.assertRaisesRegex(yaml.YAMLError, "Invalid YAML front matter") as raised: + frontmatter.loads(text) + self.assertNotIn("quote it", str(raised.exception)) + def setUp(self): self.handler = YAMLHandler() self.data = {