md-adf 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. md_adf-0.1.0/LICENSE +21 -0
  2. md_adf-0.1.0/PKG-INFO +105 -0
  3. md_adf-0.1.0/README.md +90 -0
  4. md_adf-0.1.0/pyproject.toml +38 -0
  5. md_adf-0.1.0/setup.cfg +4 -0
  6. md_adf-0.1.0/src/md_adf/__init__.py +16 -0
  7. md_adf-0.1.0/src/md_adf/adf/__init__.py +4 -0
  8. md_adf-0.1.0/src/md_adf/adf/normalize.py +1 -0
  9. md_adf-0.1.0/src/md_adf/adf/types.py +21 -0
  10. md_adf-0.1.0/src/md_adf/adf/validate.py +210 -0
  11. md_adf-0.1.0/src/md_adf/convert/__init__.py +4 -0
  12. md_adf-0.1.0/src/md_adf/convert/adf_to_markdown.py +809 -0
  13. md_adf-0.1.0/src/md_adf/convert/markdown_to_adf.py +450 -0
  14. md_adf-0.1.0/src/md_adf/diagnostics/__init__.py +3 -0
  15. md_adf-0.1.0/src/md_adf/diagnostics/diagnostic.py +17 -0
  16. md_adf-0.1.0/src/md_adf/markdown/__init__.py +1 -0
  17. md_adf-0.1.0/src/md_adf/markdown/escape.py +50 -0
  18. md_adf-0.1.0/src/md_adf/markdown/render.py +1 -0
  19. md_adf-0.1.0/src/md_adf/options.py +84 -0
  20. md_adf-0.1.0/src/md_adf/py.typed +1 -0
  21. md_adf-0.1.0/src/md_adf/schemas/adf-schema.json +2963 -0
  22. md_adf-0.1.0/src/md_adf.egg-info/PKG-INFO +105 -0
  23. md_adf-0.1.0/src/md_adf.egg-info/SOURCES.txt +30 -0
  24. md_adf-0.1.0/src/md_adf.egg-info/dependency_links.txt +1 -0
  25. md_adf-0.1.0/src/md_adf.egg-info/entry_points.txt +2 -0
  26. md_adf-0.1.0/src/md_adf.egg-info/requires.txt +2 -0
  27. md_adf-0.1.0/src/md_adf.egg-info/top_level.txt +2 -0
  28. md_adf-0.1.0/src/md_adf_cli/__main__.py +182 -0
  29. md_adf-0.1.0/tests/test_adf_validation.py +33 -0
  30. md_adf-0.1.0/tests/test_cli.py +99 -0
  31. md_adf-0.1.0/tests/test_conformance.py +27 -0
  32. md_adf-0.1.0/tests/test_options.py +66 -0
md_adf-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Markdown-ADF contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
md_adf-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,105 @@
1
+ Metadata-Version: 2.4
2
+ Name: md-adf
3
+ Version: 0.1.0
4
+ Summary: Markdown-ADF converter for Python.
5
+ Author: Markdown-ADF contributors
6
+ License-Expression: MIT
7
+ Classifier: Programming Language :: Python :: 3
8
+ Classifier: Typing :: Typed
9
+ Requires-Python: >=3.10
10
+ Description-Content-Type: text/markdown
11
+ License-File: LICENSE
12
+ Requires-Dist: jsonschema<5.0.0,>=4.26.0
13
+ Requires-Dist: mistune<4.0.0,>=3.2.1
14
+ Dynamic: license-file
15
+
16
+ # md-adf
17
+
18
+ Typed Python implementation of the Markdown-ADF converter.
19
+
20
+ ## Install
21
+
22
+ ```sh
23
+ pip install md-adf
24
+ ```
25
+
26
+ Requires Python 3.10 or newer.
27
+
28
+ ## API
29
+
30
+ ```python
31
+ from md_adf import (
32
+ adf_to_markdown,
33
+ markdown_to_adf,
34
+ parse_adf,
35
+ validate_adf,
36
+ )
37
+
38
+ markdown_result = adf_to_markdown(adf_document)
39
+ print(markdown_result.value)
40
+ print(markdown_result.diagnostics)
41
+
42
+ adf_result = markdown_to_adf("## Hello\n\nThis is **ADF**.")
43
+ print(adf_result.value)
44
+ ```
45
+
46
+ Results have this shape:
47
+
48
+ ```python
49
+ @dataclass(frozen=True)
50
+ class ConversionResult(Generic[T]):
51
+ value: T
52
+ diagnostics: list[Diagnostic]
53
+ ```
54
+
55
+ Current `ConversionOptions` support:
56
+
57
+ - `markdown_dialect`: only `gfm` is supported; other values are rejected. Dict options may also use `markdownDialect`.
58
+ - `profile`: `jira`, `confluence`, and `portableMarkdown` are accepted for API and CLI parity, but do not change Phase 1 conversion behavior yet.
59
+ - `validate_adf`: defaults to `True`; set to `False` to skip pinned schema validation for ADF-to-Markdown input. Dict options may also use `validateAdf`.
60
+ - `normalize_adf`: accepted for future normalization controls. It is currently a no-op because Phase 1 Markdown-to-ADF output is already normalized where supported. Dict options may also use `normalizeAdf`.
61
+
62
+ ADF-to-Markdown returns a string with trailing whitespace trimmed and no final newline. The CLI writes that string exactly.
63
+
64
+ ## CLI
65
+
66
+ ```sh
67
+ md-adf to-md input.adf.json --output output.md
68
+ md-adf to-adf input.md --output output.adf.json
69
+ md-adf validate-adf input.adf.json
70
+ cat input.md | md-adf to-adf --profile jira
71
+ ```
72
+
73
+ Supported profiles are `jira`, `confluence`, and `portableMarkdown`. Diagnostics are emitted as JSON lines to stderr.
74
+
75
+ ## Current Support
76
+
77
+ - ADF validation/parsing against a pinned ADF schema.
78
+ - ADF to Markdown for `doc`, `paragraph`, `heading`, `blockquote`, `bulletList`, `orderedList`, `listItem`, `codeBlock`, `rule`, simple GFM tables, task lists, media link/text fallback, rich inline text fallback for `mention`, `emoji`, `date`, and `status`, `text`, and `hardBreak`.
79
+ - Markdown to ADF for paragraphs, headings, block quotes, lists, GFM task lists, code blocks, thematic breaks, GFM tables, text, hard breaks, soft breaks, links, images as link text fallback, and raw HTML as text fallback.
80
+ - Marks for `strong`, `em`, `strike`, `code`, and `link`.
81
+ - Structured diagnostics for invalid ADF roots, unsupported ADF nodes/marks, invalid containers, invalid link marks, complex table omission, task-list fallback, image/media fallback, rich inline fallback, and dropped rich inline attributes.
82
+
83
+ ## Known Limitations
84
+
85
+ - ADF to Markdown only renders simple rectangular tables as GFM pipe tables; complex tables are omitted with diagnostics.
86
+ - Media is represented as Markdown link/text fallback; no media URL resolver or image emission exists yet.
87
+ - Markdown to ADF maps images to linked text fallback instead of ADF media nodes.
88
+ - Mixed or complex GFM task lists may fall back to ordinary list items with diagnostics.
89
+ - ADF to Markdown renders mentions, emoji, dates, and statuses as text fallbacks; Markdown to ADF keeps that text as normal text and does not recreate rich inline node IDs.
90
+ - ADF to Markdown does not yet render panels, expands, cards, layout nodes, extensions, or color/underline/subscript/superscript marks.
91
+ - Markdown raw HTML is preserved as text fallback; it is not interpreted into rich ADF.
92
+ - Unsupported ADF nodes are omitted with diagnostics.
93
+ - Markdown parser AST access is not exposed as public API yet; conversion uses real Markdown parsers internally.
94
+
95
+ ## Development
96
+
97
+ ```sh
98
+ poetry -C packages/python run python ../../tools/conformance/run-python.py
99
+ poetry -C packages/python run pytest
100
+ poetry -C packages/python run ruff check --config ../../pyproject.toml src tests ../../tools/conformance/run-python.py
101
+ poetry -C packages/python run mypy --config-file ../../pyproject.toml src tests ../../tools/conformance/run-python.py
102
+ npm run test:packages
103
+ ```
104
+
105
+ Shared conformance fixtures live in `../../fixtures` and are run by the root `just test-python` command.
md_adf-0.1.0/README.md ADDED
@@ -0,0 +1,90 @@
1
+ # md-adf
2
+
3
+ Typed Python implementation of the Markdown-ADF converter.
4
+
5
+ ## Install
6
+
7
+ ```sh
8
+ pip install md-adf
9
+ ```
10
+
11
+ Requires Python 3.10 or newer.
12
+
13
+ ## API
14
+
15
+ ```python
16
+ from md_adf import (
17
+ adf_to_markdown,
18
+ markdown_to_adf,
19
+ parse_adf,
20
+ validate_adf,
21
+ )
22
+
23
+ markdown_result = adf_to_markdown(adf_document)
24
+ print(markdown_result.value)
25
+ print(markdown_result.diagnostics)
26
+
27
+ adf_result = markdown_to_adf("## Hello\n\nThis is **ADF**.")
28
+ print(adf_result.value)
29
+ ```
30
+
31
+ Results have this shape:
32
+
33
+ ```python
34
+ @dataclass(frozen=True)
35
+ class ConversionResult(Generic[T]):
36
+ value: T
37
+ diagnostics: list[Diagnostic]
38
+ ```
39
+
40
+ Current `ConversionOptions` support:
41
+
42
+ - `markdown_dialect`: only `gfm` is supported; other values are rejected. Dict options may also use `markdownDialect`.
43
+ - `profile`: `jira`, `confluence`, and `portableMarkdown` are accepted for API and CLI parity, but do not change Phase 1 conversion behavior yet.
44
+ - `validate_adf`: defaults to `True`; set to `False` to skip pinned schema validation for ADF-to-Markdown input. Dict options may also use `validateAdf`.
45
+ - `normalize_adf`: accepted for future normalization controls. It is currently a no-op because Phase 1 Markdown-to-ADF output is already normalized where supported. Dict options may also use `normalizeAdf`.
46
+
47
+ ADF-to-Markdown returns a string with trailing whitespace trimmed and no final newline. The CLI writes that string exactly.
48
+
49
+ ## CLI
50
+
51
+ ```sh
52
+ md-adf to-md input.adf.json --output output.md
53
+ md-adf to-adf input.md --output output.adf.json
54
+ md-adf validate-adf input.adf.json
55
+ cat input.md | md-adf to-adf --profile jira
56
+ ```
57
+
58
+ Supported profiles are `jira`, `confluence`, and `portableMarkdown`. Diagnostics are emitted as JSON lines to stderr.
59
+
60
+ ## Current Support
61
+
62
+ - ADF validation/parsing against a pinned ADF schema.
63
+ - ADF to Markdown for `doc`, `paragraph`, `heading`, `blockquote`, `bulletList`, `orderedList`, `listItem`, `codeBlock`, `rule`, simple GFM tables, task lists, media link/text fallback, rich inline text fallback for `mention`, `emoji`, `date`, and `status`, `text`, and `hardBreak`.
64
+ - Markdown to ADF for paragraphs, headings, block quotes, lists, GFM task lists, code blocks, thematic breaks, GFM tables, text, hard breaks, soft breaks, links, images as link text fallback, and raw HTML as text fallback.
65
+ - Marks for `strong`, `em`, `strike`, `code`, and `link`.
66
+ - Structured diagnostics for invalid ADF roots, unsupported ADF nodes/marks, invalid containers, invalid link marks, complex table omission, task-list fallback, image/media fallback, rich inline fallback, and dropped rich inline attributes.
67
+
68
+ ## Known Limitations
69
+
70
+ - ADF to Markdown only renders simple rectangular tables as GFM pipe tables; complex tables are omitted with diagnostics.
71
+ - Media is represented as Markdown link/text fallback; no media URL resolver or image emission exists yet.
72
+ - Markdown to ADF maps images to linked text fallback instead of ADF media nodes.
73
+ - Mixed or complex GFM task lists may fall back to ordinary list items with diagnostics.
74
+ - ADF to Markdown renders mentions, emoji, dates, and statuses as text fallbacks; Markdown to ADF keeps that text as normal text and does not recreate rich inline node IDs.
75
+ - ADF to Markdown does not yet render panels, expands, cards, layout nodes, extensions, or color/underline/subscript/superscript marks.
76
+ - Markdown raw HTML is preserved as text fallback; it is not interpreted into rich ADF.
77
+ - Unsupported ADF nodes are omitted with diagnostics.
78
+ - Markdown parser AST access is not exposed as public API yet; conversion uses real Markdown parsers internally.
79
+
80
+ ## Development
81
+
82
+ ```sh
83
+ poetry -C packages/python run python ../../tools/conformance/run-python.py
84
+ poetry -C packages/python run pytest
85
+ poetry -C packages/python run ruff check --config ../../pyproject.toml src tests ../../tools/conformance/run-python.py
86
+ poetry -C packages/python run mypy --config-file ../../pyproject.toml src tests ../../tools/conformance/run-python.py
87
+ npm run test:packages
88
+ ```
89
+
90
+ Shared conformance fixtures live in `../../fixtures` and are run by the root `just test-python` command.
@@ -0,0 +1,38 @@
1
+ [build-system]
2
+ requires = ["setuptools>=69", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "md-adf"
7
+ version = "0.1.0"
8
+ description = "Markdown-ADF converter for Python."
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = "MIT"
12
+ authors = [
13
+ { name = "Markdown-ADF contributors" }
14
+ ]
15
+ classifiers = [
16
+ "Programming Language :: Python :: 3",
17
+ "Typing :: Typed"
18
+ ]
19
+ dependencies = [
20
+ "jsonschema>=4.26.0,<5.0.0",
21
+ "mistune>=3.2.1,<4.0.0"
22
+ ]
23
+
24
+ [project.scripts]
25
+ md-adf = "md_adf_cli.__main__:main"
26
+
27
+ [tool.poetry.group.dev.dependencies]
28
+ mypy = ">=1.0"
29
+ pytest = ">=8.0"
30
+ ruff = ">=0.4"
31
+ types-jsonschema = "^4.26.0.20260408"
32
+
33
+ [tool.setuptools.packages.find]
34
+ where = ["src"]
35
+ include = ["md_adf*", "md_adf_cli*"]
36
+
37
+ [tool.setuptools.package-data]
38
+ md_adf = ["py.typed", "schemas/*.json"]
md_adf-0.1.0/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,16 @@
1
+ """Markdown-ADF converter package."""
2
+
3
+ from .adf.validate import parse_adf, validate_adf
4
+ from .convert.adf_to_markdown import adf_to_markdown
5
+ from .convert.markdown_to_adf import markdown_to_adf
6
+ from .options import ConversionOptions, ConversionOptionsInput, ConversionResult
7
+
8
+ __all__ = [
9
+ "ConversionResult",
10
+ "ConversionOptions",
11
+ "ConversionOptionsInput",
12
+ "adf_to_markdown",
13
+ "markdown_to_adf",
14
+ "parse_adf",
15
+ "validate_adf",
16
+ ]
@@ -0,0 +1,4 @@
1
+ from .types import AdfDocument, AdfNode
2
+ from .validate import ValidationResult, parse_adf, validate_adf
3
+
4
+ __all__ = ["AdfDocument", "AdfNode", "ValidationResult", "parse_adf", "validate_adf"]
@@ -0,0 +1 @@
1
+ """ADF normalization placeholder."""
@@ -0,0 +1,21 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, TypedDict
4
+
5
+
6
+ class AdfNode(TypedDict, total=False):
7
+ """Minimal structural representation for ADF nodes used by the converters."""
8
+
9
+ type: str
10
+ attrs: dict[str, Any]
11
+ marks: list[dict[str, Any]]
12
+ content: list["AdfNode"]
13
+ text: str
14
+
15
+
16
+ class AdfDocument(TypedDict):
17
+ """Root ADF document shape accepted and emitted by this package."""
18
+
19
+ version: int
20
+ type: str
21
+ content: list[AdfNode]
@@ -0,0 +1,210 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ import json
5
+ from pathlib import Path
6
+ from typing import Any, cast
7
+
8
+ from jsonschema import Draft4Validator
9
+ from jsonschema.exceptions import ValidationError
10
+
11
+ from .types import AdfDocument
12
+
13
+
14
+ @dataclass(frozen=True)
15
+ class ParseAdfOptions:
16
+ """Options controlling how unknown values are parsed as ADF."""
17
+
18
+ validate_adf: bool = True
19
+
20
+
21
+ @dataclass(frozen=True)
22
+ class ValidationResult:
23
+ """Structured success/error result for ADF validation."""
24
+
25
+ valid: bool
26
+ errors: list[str] = field(default_factory=list)
27
+
28
+
29
+ def parse_adf(value: Any, options: Any = None) -> AdfDocument:
30
+ """Parse an unknown value as an ADF document, optionally validating schema."""
31
+
32
+ if (
33
+ isinstance(value, dict)
34
+ and value.get("version") == 1
35
+ and value.get("type") == "doc"
36
+ and isinstance(value.get("content"), list)
37
+ ):
38
+ if _validate_enabled(options):
39
+ errors = list(_VALIDATOR.iter_errors(value))
40
+ if errors:
41
+ raise ValueError(_format_schema_error(_most_specific_error(errors, value)))
42
+ return cast(AdfDocument, value)
43
+
44
+ raise ValueError("Invalid ADF root: expected doc version 1.")
45
+
46
+
47
+ def validate_adf(value: Any, options: Any = None) -> ValidationResult:
48
+ """Validate an unknown value as ADF without raising validation errors."""
49
+
50
+ try:
51
+ parse_adf(value, options)
52
+ except ValueError as exc:
53
+ return ValidationResult(valid=False, errors=[str(exc)])
54
+
55
+ return ValidationResult(valid=True)
56
+
57
+
58
+ def _schema_path() -> Path:
59
+ """Find the pinned ADF JSON Schema in source or packaged layouts."""
60
+
61
+ here = Path(__file__).resolve()
62
+ candidates = [
63
+ here.parents[1] / "schemas" / "adf-schema.json",
64
+ here.parents[5] / "schemas" / "adf-schema.json",
65
+ ]
66
+ for candidate in candidates:
67
+ if candidate.exists():
68
+ return candidate
69
+
70
+ raise RuntimeError("Pinned ADF JSON Schema not found.")
71
+
72
+
73
+ _SCHEMA = json.loads(_schema_path().read_text(encoding="utf-8"))
74
+ _VALIDATOR = Draft4Validator(_SCHEMA)
75
+
76
+
77
+ def _validate_enabled(options: Any) -> bool:
78
+ """Read the validate_adf option from dataclass or mapping-style options."""
79
+
80
+ if options is None:
81
+ return True
82
+ if isinstance(options, ParseAdfOptions):
83
+ return options.validate_adf
84
+ if hasattr(options, "validate_adf"):
85
+ return bool(options.validate_adf)
86
+ value = options.get("validate_adf", options.get("validateAdf", True))
87
+ return bool(value)
88
+
89
+
90
+ def _format_schema_error(error: ValidationError) -> str:
91
+ """Convert a jsonschema validation error into a concise user message."""
92
+
93
+ path = _json_pointer(error.absolute_path)
94
+ location = f" at {path}" if path else ""
95
+
96
+ if error.validator == "required":
97
+ missing = _required_property(error.message)
98
+ return f"Invalid ADF{location}: missing required property '{missing}'."
99
+ if error.validator == "additionalProperties":
100
+ property_name = _additional_property(error.message)
101
+ return f"Invalid ADF{location}: unexpected property '{property_name}'."
102
+ if error.validator == "enum":
103
+ return f"Invalid ADF{location}: value is not allowed."
104
+ if error.validator == "type":
105
+ return f"Invalid ADF{location}: expected {error.validator_value}."
106
+ if error.validator == "minLength":
107
+ return f"Invalid ADF{location}: expected a non-empty string."
108
+ if error.validator == "minItems":
109
+ return f"Invalid ADF{location}: expected at least one item."
110
+ return f"Invalid ADF{location}: failed schema constraint '{error.validator}'."
111
+
112
+
113
+ def _most_specific_error(errors: list[ValidationError], value: Any) -> ValidationError:
114
+ """Choose the schema error that best identifies the invalid ADF location."""
115
+
116
+ candidates = _leaf_errors(errors)
117
+ relevant = [error for error in candidates if error.validator not in {"anyOf", "oneOf"}]
118
+ matching_type_error = next(
119
+ (error for error in relevant if _error_matches_actual_type(error, value)),
120
+ None,
121
+ )
122
+ if matching_type_error is not None:
123
+ return matching_type_error
124
+ return max(relevant or candidates, key=_error_specificity)
125
+
126
+
127
+ def _leaf_errors(errors: list[ValidationError]) -> list[ValidationError]:
128
+ """Flatten nested anyOf/oneOf validation contexts to leaf errors."""
129
+
130
+ leaves: list[ValidationError] = []
131
+ for error in errors:
132
+ if error.context:
133
+ leaves.extend(_leaf_errors(list(error.context)))
134
+ else:
135
+ leaves.append(error)
136
+ return leaves
137
+
138
+
139
+ def _json_pointer(path: Any) -> str:
140
+ """Render a jsonschema path as an escaped JSON Pointer."""
141
+
142
+ parts = [str(part).replace("~", "~0").replace("/", "~1") for part in path]
143
+ return "/" + "/".join(parts) if parts else ""
144
+
145
+
146
+ def _error_specificity(error: ValidationError) -> int:
147
+ """Score schema errors so deeper paths are considered more specific."""
148
+
149
+ base = len(error.absolute_path)
150
+ if error.validator == "required" and "/attrs" in _json_pointer(error.absolute_path):
151
+ return base + 1
152
+ return base
153
+
154
+
155
+ def _additional_property(message: str) -> str:
156
+ """Extract an additional-property name from a jsonschema error message."""
157
+
158
+ marker = "'"
159
+ start = message.find(marker)
160
+ end = message.find(marker, start + 1)
161
+ return message[start + 1 : end] if start >= 0 and end > start else "unknown"
162
+
163
+
164
+ def _error_matches_actual_type(error: ValidationError, value: Any) -> bool:
165
+ """Check whether a required-property error matches the node's actual type."""
166
+
167
+ if error.validator != "required":
168
+ return False
169
+
170
+ parent_path = list(error.absolute_path)
171
+ if parent_path[-1:] == ["attrs"]:
172
+ parent_path = parent_path[:-1]
173
+ parent = _value_at_path(value, parent_path)
174
+ if (
175
+ isinstance(parent, dict)
176
+ and parent.get("type") == "link"
177
+ and _required_property(error.message) == "href"
178
+ ):
179
+ return True
180
+
181
+ schema_path = list(error.schema_path)
182
+ if len(schema_path) < 2 or schema_path[0] != "definitions":
183
+ return False
184
+
185
+ definition = str(schema_path[1])
186
+ expected_type = definition.removesuffix("_mark").removesuffix("_node")
187
+ return isinstance(parent, dict) and parent.get("type") == expected_type
188
+
189
+
190
+ def _value_at_path(value: Any, path: list[Any]) -> Any:
191
+ """Resolve a jsonschema path against a nested Python value."""
192
+
193
+ current = value
194
+ for part in path:
195
+ if isinstance(current, list) and isinstance(part, int):
196
+ current = current[part]
197
+ elif isinstance(current, dict):
198
+ current = current.get(part)
199
+ else:
200
+ return None
201
+ return current
202
+
203
+
204
+ def _required_property(message: str) -> str:
205
+ """Extract a required-property name from a jsonschema error message."""
206
+
207
+ marker = "'"
208
+ start = message.find(marker)
209
+ end = message.find(marker, start + 1)
210
+ return message[start + 1 : end] if start >= 0 and end > start else "unknown"
@@ -0,0 +1,4 @@
1
+ from .adf_to_markdown import adf_to_markdown
2
+ from .markdown_to_adf import markdown_to_adf
3
+
4
+ __all__ = ["adf_to_markdown", "markdown_to_adf"]