sphinx-mkdocs-migrate 0.0.1.dev0__py3-none-any.whl
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.
- sphinx_mkdocs_migrate/__init__.py +8 -0
- sphinx_mkdocs_migrate/analyzer/__init__.py +17 -0
- sphinx_mkdocs_migrate/analyzer/ci.py +223 -0
- sphinx_mkdocs_migrate/analyzer/dependencies.py +134 -0
- sphinx_mkdocs_migrate/analyzer/markdown.py +148 -0
- sphinx_mkdocs_migrate/analyzer/mkdocs.py +263 -0
- sphinx_mkdocs_migrate/analyzer/models.py +348 -0
- sphinx_mkdocs_migrate/analyzer/navigation.py +108 -0
- sphinx_mkdocs_migrate/analyzer/project.py +511 -0
- sphinx_mkdocs_migrate/cli.py +507 -0
- sphinx_mkdocs_migrate/parsing/doc_ir.py +533 -0
- sphinx_mkdocs_migrate/parsing/flow_extractor.py +457 -0
- sphinx_mkdocs_migrate/parsing/html_flow_parser.py +349 -0
- sphinx_mkdocs_migrate/parsing/markdown.py +22 -0
- sphinx_mkdocs_migrate/parsing/markdown_ir.py +49 -0
- sphinx_mkdocs_migrate/parsing/markdown_it_adapter.py +496 -0
- sphinx_mkdocs_migrate/parsing/requirements.py +155 -0
- sphinx_mkdocs_migrate/planner/accountability.py +111 -0
- sphinx_mkdocs_migrate/planner/ci.py +142 -0
- sphinx_mkdocs_migrate/planner/conf_builder.py +183 -0
- sphinx_mkdocs_migrate/planner/models.py +379 -0
- sphinx_mkdocs_migrate/planner/planner.py +1867 -0
- sphinx_mkdocs_migrate/planner/policy.py +474 -0
- sphinx_mkdocs_migrate/planner/theme_constants.py +70 -0
- sphinx_mkdocs_migrate/planner/toctree.py +158 -0
- sphinx_mkdocs_migrate/py.typed +1 -0
- sphinx_mkdocs_migrate/rules/catalog.py +154 -0
- sphinx_mkdocs_migrate/rules/engine.py +94 -0
- sphinx_mkdocs_migrate/rules/models.py +176 -0
- sphinx_mkdocs_migrate/transformer/engine.py +897 -0
- sphinx_mkdocs_migrate/transformer/models.py +59 -0
- sphinx_mkdocs_migrate/transformer/myst_transformer.py +393 -0
- sphinx_mkdocs_migrate/validator/models.py +40 -0
- sphinx_mkdocs_migrate/validator/verifier.py +377 -0
- sphinx_mkdocs_migrate-0.0.1.dev0.dist-info/METADATA +199 -0
- sphinx_mkdocs_migrate-0.0.1.dev0.dist-info/RECORD +39 -0
- sphinx_mkdocs_migrate-0.0.1.dev0.dist-info/WHEEL +4 -0
- sphinx_mkdocs_migrate-0.0.1.dev0.dist-info/entry_points.txt +2 -0
- sphinx_mkdocs_migrate-0.0.1.dev0.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Analyzer subsystem module."""
|
|
2
|
+
|
|
3
|
+
from .project import ProjectAnalyzer
|
|
4
|
+
from .models import (
|
|
5
|
+
ProjectAnalysisReport,
|
|
6
|
+
Classification,
|
|
7
|
+
ConstructFinding,
|
|
8
|
+
SubsystemSummary,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"ProjectAnalyzer",
|
|
13
|
+
"ProjectAnalysisReport",
|
|
14
|
+
"Classification",
|
|
15
|
+
"ConstructFinding",
|
|
16
|
+
"SubsystemSummary",
|
|
17
|
+
]
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
"""Subsystem analyzer for CI/CD, local test runners, and deployment workflows."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Optional, List, Dict, Tuple
|
|
6
|
+
from .models import CIAnalysis
|
|
7
|
+
|
|
8
|
+
DEFAULT_CHECKOUT_TAG = "v7.0.1"
|
|
9
|
+
DEFAULT_CHECKOUT_SHA = "3d3c42e5aac5ba805825da76410c181273ba90b1"
|
|
10
|
+
DEFAULT_SETUP_UV_TAG = "v10.2.0"
|
|
11
|
+
DEFAULT_SETUP_UV_SHA = "c18668ad3cf93ea998bef934396af7bb5c839dc7"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def resolve_github_action_ref(
|
|
15
|
+
action_repo: str, fallback_tag: str, fallback_sha: str
|
|
16
|
+
) -> Tuple[str, str]:
|
|
17
|
+
"""Resolves the latest release tag and commit SHA for a GitHub Action repository via git ls-remote."""
|
|
18
|
+
import subprocess
|
|
19
|
+
|
|
20
|
+
try:
|
|
21
|
+
url = f"https://github.com/{action_repo}.git"
|
|
22
|
+
out = subprocess.check_output(
|
|
23
|
+
["git", "ls-remote", "--tags", url],
|
|
24
|
+
text=True,
|
|
25
|
+
timeout=5,
|
|
26
|
+
stdin=subprocess.DEVNULL,
|
|
27
|
+
stderr=subprocess.DEVNULL,
|
|
28
|
+
)
|
|
29
|
+
tag_shas: Dict[str, str] = {}
|
|
30
|
+
for line in out.splitlines():
|
|
31
|
+
parts = line.split()
|
|
32
|
+
if len(parts) >= 2:
|
|
33
|
+
sha, ref = parts[0], parts[1]
|
|
34
|
+
m = re.match(r"^refs/tags/(v?[0-9]+(?:\.[0-9]+)*)(\^\{\})?$", ref)
|
|
35
|
+
if m:
|
|
36
|
+
tag = m.group(1)
|
|
37
|
+
if ref.endswith("^{}"):
|
|
38
|
+
tag_shas[tag] = sha
|
|
39
|
+
elif tag not in tag_shas:
|
|
40
|
+
tag_shas[tag] = sha
|
|
41
|
+
if tag_shas:
|
|
42
|
+
|
|
43
|
+
def semver_key(t: str):
|
|
44
|
+
return [int(x) for x in re.findall(r"[0-9]+", t)]
|
|
45
|
+
|
|
46
|
+
sorted_tags = sorted(tag_shas.keys(), key=semver_key)
|
|
47
|
+
latest_tag = sorted_tags[-1]
|
|
48
|
+
return latest_tag, tag_shas[latest_tag]
|
|
49
|
+
except Exception:
|
|
50
|
+
pass
|
|
51
|
+
return fallback_tag, fallback_sha
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class CIAnalyzer:
|
|
55
|
+
"""Discovers and inspects project CI/CD workflows, runners (tox/nox/make), and hosted doc configs."""
|
|
56
|
+
|
|
57
|
+
def __init__(self, project_root: Path):
|
|
58
|
+
self.project_root = project_root
|
|
59
|
+
|
|
60
|
+
def analyze(self) -> CIAnalysis:
|
|
61
|
+
# 1. GitHub Actions (.github/workflows/*.yml)
|
|
62
|
+
github_workflows = self.project_root / ".github" / "workflows"
|
|
63
|
+
workflow_files: List[str] = []
|
|
64
|
+
docs_workflow_files: List[str] = []
|
|
65
|
+
has_deploy = False
|
|
66
|
+
has_tox_in_ci = False
|
|
67
|
+
has_uv_in_ci = False
|
|
68
|
+
has_gh_pages_action = False
|
|
69
|
+
checkout_ref: Optional[str] = None
|
|
70
|
+
setup_uv_ref: Optional[str] = None
|
|
71
|
+
checkout_pinned_ref: Optional[str] = None
|
|
72
|
+
setup_uv_pinned_ref: Optional[str] = None
|
|
73
|
+
github_actions_detected = github_workflows.is_dir()
|
|
74
|
+
|
|
75
|
+
if github_actions_detected:
|
|
76
|
+
chk_tag, chk_sha = resolve_github_action_ref(
|
|
77
|
+
"actions/checkout", DEFAULT_CHECKOUT_TAG, DEFAULT_CHECKOUT_SHA
|
|
78
|
+
)
|
|
79
|
+
uv_tag, uv_sha = resolve_github_action_ref(
|
|
80
|
+
"astral-sh/setup-uv", DEFAULT_SETUP_UV_TAG, DEFAULT_SETUP_UV_SHA
|
|
81
|
+
)
|
|
82
|
+
checkout_pinned_ref = f"{chk_sha} # {chk_tag}" if chk_sha else chk_tag
|
|
83
|
+
setup_uv_pinned_ref = f"{uv_sha} # {uv_tag}" if uv_sha else uv_tag
|
|
84
|
+
|
|
85
|
+
if github_actions_detected:
|
|
86
|
+
for yml_file in sorted(github_workflows.glob("*.y*ml")):
|
|
87
|
+
rel = yml_file.relative_to(self.project_root).as_posix()
|
|
88
|
+
workflow_files.append(rel)
|
|
89
|
+
try:
|
|
90
|
+
content = yml_file.read_text(encoding="utf-8")
|
|
91
|
+
if (
|
|
92
|
+
"mkdocs gh-deploy" in content
|
|
93
|
+
or "mkdocs build" in content
|
|
94
|
+
or "mike deploy" in content
|
|
95
|
+
):
|
|
96
|
+
has_deploy = True
|
|
97
|
+
docs_workflow_files.append(rel)
|
|
98
|
+
if "tox" in content or "uvx tox" in content:
|
|
99
|
+
has_tox_in_ci = True
|
|
100
|
+
if (
|
|
101
|
+
"astral-sh/setup-uv" in content
|
|
102
|
+
or "setup-uv" in content
|
|
103
|
+
or "uv run" in content
|
|
104
|
+
):
|
|
105
|
+
has_uv_in_ci = True
|
|
106
|
+
if (
|
|
107
|
+
"peaceiris/actions-gh-pages" in content
|
|
108
|
+
or "actions/deploy-pages" in content
|
|
109
|
+
):
|
|
110
|
+
has_gh_pages_action = True
|
|
111
|
+
|
|
112
|
+
# Extract current action version refs
|
|
113
|
+
m_chk = re.search(r"uses:\s*actions/checkout@([^\s\n]+)", content)
|
|
114
|
+
if m_chk and not checkout_ref:
|
|
115
|
+
checkout_ref = m_chk.group(1).split("#")[0].strip()
|
|
116
|
+
m_uv = re.search(r"uses:\s*astral-sh/setup-uv@([^\s\n]+)", content)
|
|
117
|
+
if m_uv and not setup_uv_ref:
|
|
118
|
+
setup_uv_ref = m_uv.group(1).split("#")[0].strip()
|
|
119
|
+
except Exception:
|
|
120
|
+
pass
|
|
121
|
+
|
|
122
|
+
# 2. Local test & doc runners: tox.ini
|
|
123
|
+
tox_ini = self.project_root / "tox.ini"
|
|
124
|
+
has_tox = tox_ini.is_file()
|
|
125
|
+
tox_file = (
|
|
126
|
+
tox_ini.relative_to(self.project_root).as_posix() if has_tox else None
|
|
127
|
+
)
|
|
128
|
+
tox_has_docs_env = False
|
|
129
|
+
tox_docs_commands: List[str] = []
|
|
130
|
+
tox_dependency_spec: Optional[str] = None
|
|
131
|
+
|
|
132
|
+
if has_tox:
|
|
133
|
+
try:
|
|
134
|
+
tox_text = tox_ini.read_text(encoding="utf-8")
|
|
135
|
+
if "[testenv:docs]" in tox_text:
|
|
136
|
+
tox_has_docs_env = True
|
|
137
|
+
# Extract section content
|
|
138
|
+
m_sec = re.search(
|
|
139
|
+
r"\[testenv:docs\](.*?)(?=\n\[|\Z)", tox_text, re.DOTALL
|
|
140
|
+
)
|
|
141
|
+
if m_sec:
|
|
142
|
+
sec_text = m_sec.group(1)
|
|
143
|
+
for line in sec_text.splitlines():
|
|
144
|
+
line_s = line.strip()
|
|
145
|
+
if (
|
|
146
|
+
line_s.startswith("dependency_groups")
|
|
147
|
+
or line_s.startswith("extras")
|
|
148
|
+
or line_s.startswith("deps")
|
|
149
|
+
):
|
|
150
|
+
tox_dependency_spec = line_s
|
|
151
|
+
elif line_s.startswith("commands =") or (
|
|
152
|
+
line.startswith(" ")
|
|
153
|
+
and ("sphinx-build" in line_s or "mkdocs" in line_s)
|
|
154
|
+
):
|
|
155
|
+
tox_docs_commands.append(line_s)
|
|
156
|
+
elif "mkdocs" in tox_text:
|
|
157
|
+
tox_docs_commands.append("mkdocs")
|
|
158
|
+
except Exception:
|
|
159
|
+
pass
|
|
160
|
+
|
|
161
|
+
# 3. ReadTheDocs (.readthedocs.yaml or .readthedocs.yml)
|
|
162
|
+
rtd_config = self.project_root / ".readthedocs.yaml"
|
|
163
|
+
if not rtd_config.exists():
|
|
164
|
+
rtd_config = self.project_root / ".readthedocs.yml"
|
|
165
|
+
has_rtd = rtd_config.is_file()
|
|
166
|
+
rtd_file = (
|
|
167
|
+
rtd_config.relative_to(self.project_root).as_posix() if has_rtd else None
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
# 4. Nox runner (noxfile.py)
|
|
171
|
+
noxfile = self.project_root / "noxfile.py"
|
|
172
|
+
has_nox = noxfile.is_file()
|
|
173
|
+
nox_file = (
|
|
174
|
+
noxfile.relative_to(self.project_root).as_posix() if has_nox else None
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
# 5. Makefile
|
|
178
|
+
makefile = self.project_root / "Makefile"
|
|
179
|
+
has_makefile = makefile.is_file()
|
|
180
|
+
|
|
181
|
+
# 6. Pre-commit (.pre-commit-config.yaml)
|
|
182
|
+
pre_commit = self.project_root / ".pre-commit-config.yaml"
|
|
183
|
+
has_pre_commit = pre_commit.is_file()
|
|
184
|
+
|
|
185
|
+
# 7. GitLab CI (.gitlab-ci.yml)
|
|
186
|
+
gitlab_ci = self.project_root / ".gitlab-ci.yml"
|
|
187
|
+
gitlab_ci_detected = gitlab_ci.is_file()
|
|
188
|
+
|
|
189
|
+
# Primary CI system resolution
|
|
190
|
+
ci_system = None
|
|
191
|
+
if github_actions_detected:
|
|
192
|
+
ci_system = "github_actions"
|
|
193
|
+
elif gitlab_ci_detected:
|
|
194
|
+
ci_system = "gitlab_ci"
|
|
195
|
+
elif has_rtd:
|
|
196
|
+
ci_system = "readthedocs"
|
|
197
|
+
|
|
198
|
+
return CIAnalysis(
|
|
199
|
+
ci_system=ci_system,
|
|
200
|
+
workflow_files=workflow_files,
|
|
201
|
+
docs_workflow_files=docs_workflow_files,
|
|
202
|
+
has_mkdocs_deploy=has_deploy,
|
|
203
|
+
readthedocs_detected=has_rtd,
|
|
204
|
+
rtd_config_file=rtd_file,
|
|
205
|
+
github_actions_detected=github_actions_detected,
|
|
206
|
+
has_tox_in_ci=has_tox_in_ci,
|
|
207
|
+
has_uv_in_ci=has_uv_in_ci,
|
|
208
|
+
has_gh_pages_action=has_gh_pages_action,
|
|
209
|
+
checkout_action_ref=checkout_ref,
|
|
210
|
+
setup_uv_action_ref=setup_uv_ref,
|
|
211
|
+
checkout_pinned_ref=checkout_pinned_ref,
|
|
212
|
+
setup_uv_pinned_ref=setup_uv_pinned_ref,
|
|
213
|
+
has_tox=has_tox,
|
|
214
|
+
tox_file=tox_file,
|
|
215
|
+
tox_has_docs_env=tox_has_docs_env,
|
|
216
|
+
tox_docs_commands=tox_docs_commands,
|
|
217
|
+
tox_dependency_spec=tox_dependency_spec,
|
|
218
|
+
has_nox=has_nox,
|
|
219
|
+
nox_file=nox_file,
|
|
220
|
+
has_makefile=has_makefile,
|
|
221
|
+
has_pre_commit=has_pre_commit,
|
|
222
|
+
gitlab_ci_detected=gitlab_ci_detected,
|
|
223
|
+
)
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import Optional, Dict
|
|
3
|
+
import importlib.metadata
|
|
4
|
+
from ..parsing.requirements import (
|
|
5
|
+
parse_pyproject_toml,
|
|
6
|
+
parse_requirements_txt,
|
|
7
|
+
NormalizedManifest,
|
|
8
|
+
)
|
|
9
|
+
from .models import DependencyAnalysis, VersionEnvironment, PackageVersionInfo
|
|
10
|
+
|
|
11
|
+
KNOWN_MKDOCS_PACKAGES = {
|
|
12
|
+
"mkdocs",
|
|
13
|
+
"mkdocs-material",
|
|
14
|
+
"mkdocstrings",
|
|
15
|
+
"mkdocstrings-python",
|
|
16
|
+
"mkdocs-material-extensions",
|
|
17
|
+
"pymdown-extensions",
|
|
18
|
+
"mkdocs-mermaid2-plugin",
|
|
19
|
+
"mike",
|
|
20
|
+
"mkdocs-autorefs",
|
|
21
|
+
"mkdocs-literate-nav",
|
|
22
|
+
"mkdocs-macros-plugin",
|
|
23
|
+
"mdx_truly_sane_lists",
|
|
24
|
+
"mkdocs-awesome-pages-plugin",
|
|
25
|
+
"mkdocs-gen-files",
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class DependencyAnalyzer:
|
|
30
|
+
def __init__(self, project_root: Path):
|
|
31
|
+
self.project_root = project_root
|
|
32
|
+
|
|
33
|
+
def analyze(self) -> tuple[Optional[DependencyAnalysis], VersionEnvironment]:
|
|
34
|
+
pyproject = self.project_root / "pyproject.toml"
|
|
35
|
+
docs_req = self.project_root / "docs" / "requirements.txt"
|
|
36
|
+
req = self.project_root / "requirements.txt"
|
|
37
|
+
|
|
38
|
+
manifest: Optional[NormalizedManifest] = None
|
|
39
|
+
if pyproject.exists():
|
|
40
|
+
manifest = parse_pyproject_toml(pyproject)
|
|
41
|
+
elif docs_req.exists():
|
|
42
|
+
manifest = parse_requirements_txt(docs_req)
|
|
43
|
+
elif req.exists():
|
|
44
|
+
manifest = parse_requirements_txt(req)
|
|
45
|
+
|
|
46
|
+
if not manifest:
|
|
47
|
+
return None, VersionEnvironment()
|
|
48
|
+
|
|
49
|
+
to_remove = []
|
|
50
|
+
source_group_type: Optional[str] = None
|
|
51
|
+
source_group_name: Optional[str] = None
|
|
52
|
+
for dep in manifest.dependencies:
|
|
53
|
+
if dep.name in KNOWN_MKDOCS_PACKAGES:
|
|
54
|
+
to_remove.append(dep.name)
|
|
55
|
+
if not source_group_name and dep.source_location:
|
|
56
|
+
if dep.source_location.startswith("dependency-groups."):
|
|
57
|
+
source_group_type = "dependency-groups"
|
|
58
|
+
source_group_name = dep.source_location.split(".", 1)[1]
|
|
59
|
+
elif dep.source_location.startswith(
|
|
60
|
+
"project.optional-dependencies."
|
|
61
|
+
):
|
|
62
|
+
source_group_type = "optional-dependencies"
|
|
63
|
+
source_group_name = dep.source_location.split(".", 2)[2]
|
|
64
|
+
elif "requirements" in dep.source_location:
|
|
65
|
+
source_group_type = "requirements"
|
|
66
|
+
source_group_name = dep.source_location
|
|
67
|
+
|
|
68
|
+
suggested_to_add = ["sphinx", "myst-parser", "sphinx-design"]
|
|
69
|
+
if "mkdocs-material" in to_remove:
|
|
70
|
+
suggested_to_add.append("sphinx-immaterial")
|
|
71
|
+
|
|
72
|
+
dep_analysis = DependencyAnalysis(
|
|
73
|
+
manifest_type=manifest.manifest_type,
|
|
74
|
+
detected_packages_to_remove=sorted(list(set(to_remove))),
|
|
75
|
+
suggested_packages_to_add=suggested_to_add,
|
|
76
|
+
source_group_type=source_group_type,
|
|
77
|
+
source_group_name=source_group_name,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
packages_info: Dict[str, PackageVersionInfo] = {}
|
|
81
|
+
plugin_vers: Dict[str, str] = {}
|
|
82
|
+
mkdocs_ver = None
|
|
83
|
+
|
|
84
|
+
import sys
|
|
85
|
+
|
|
86
|
+
current_py_ver = f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}"
|
|
87
|
+
|
|
88
|
+
for dep in manifest.dependencies:
|
|
89
|
+
if dep.name == "mkdocs" or dep.name in KNOWN_MKDOCS_PACKAGES:
|
|
90
|
+
# Check if installed in active environment
|
|
91
|
+
installed_ver = None
|
|
92
|
+
status = "DECLARED_ONLY"
|
|
93
|
+
res_source = "MANIFEST_DECLARATION"
|
|
94
|
+
satisfies_constraint = None
|
|
95
|
+
|
|
96
|
+
try:
|
|
97
|
+
installed_ver = importlib.metadata.version(dep.name)
|
|
98
|
+
status = "INSTALLED_RESOLVED"
|
|
99
|
+
res_source = "CURRENT_PYTHON_ENVIRONMENT"
|
|
100
|
+
# Basic constraint check if `>=` specified
|
|
101
|
+
if ">=" in dep.raw_spec:
|
|
102
|
+
req_ver = dep.raw_spec.split(">=")[-1].strip()
|
|
103
|
+
satisfies_constraint = installed_ver >= req_ver
|
|
104
|
+
else:
|
|
105
|
+
satisfies_constraint = True
|
|
106
|
+
except Exception:
|
|
107
|
+
pass
|
|
108
|
+
|
|
109
|
+
pkg_info = PackageVersionInfo(
|
|
110
|
+
package_name=dep.name,
|
|
111
|
+
declared_spec=dep.raw_spec,
|
|
112
|
+
resolved_version=installed_ver,
|
|
113
|
+
resolution_status=status,
|
|
114
|
+
resolution_source=res_source,
|
|
115
|
+
satisfies_declared_constraint=satisfies_constraint,
|
|
116
|
+
)
|
|
117
|
+
packages_info[dep.name] = pkg_info
|
|
118
|
+
|
|
119
|
+
if dep.name == "mkdocs":
|
|
120
|
+
mkdocs_ver = dep.raw_spec
|
|
121
|
+
else:
|
|
122
|
+
plugin_vers[dep.name] = dep.raw_spec
|
|
123
|
+
|
|
124
|
+
version_env = VersionEnvironment(
|
|
125
|
+
python_version=manifest.python_requirement,
|
|
126
|
+
python_constraint=manifest.python_requirement,
|
|
127
|
+
python_resolved_version=current_py_ver,
|
|
128
|
+
python_resolution_status="INSTALLED_RESOLVED",
|
|
129
|
+
packages=packages_info,
|
|
130
|
+
detected_mkdocs_version=mkdocs_ver,
|
|
131
|
+
detected_plugin_versions=plugin_vers,
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
return dep_analysis, version_env
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"""Markdown AST analyzer inspecting DocumentIR nodes to extract migration findings."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import List, Tuple, Optional
|
|
5
|
+
from ..parsing.markdown import MarkdownParser
|
|
6
|
+
from ..parsing.markdown_ir import NodeKind, BaseIRNode
|
|
7
|
+
from .models import ConstructFinding, Classification
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class MarkdownAnalyzer:
|
|
11
|
+
"""Analyzes Markdown files by parsing them into DocumentIR and recording migration findings."""
|
|
12
|
+
|
|
13
|
+
def __init__(self, project_root: Optional[Path] = None):
|
|
14
|
+
self.project_root = project_root
|
|
15
|
+
self.parser = MarkdownParser()
|
|
16
|
+
|
|
17
|
+
def analyze_file(self, file_path: Path) -> List[ConstructFinding]:
|
|
18
|
+
if not file_path.exists():
|
|
19
|
+
return []
|
|
20
|
+
|
|
21
|
+
doc_ir = self.parser.parse_file(file_path)
|
|
22
|
+
findings: List[ConstructFinding] = []
|
|
23
|
+
|
|
24
|
+
def visit_node(node: BaseIRNode):
|
|
25
|
+
snippet = (
|
|
26
|
+
node.raw_text.splitlines()[0]
|
|
27
|
+
if node.raw_text.splitlines()
|
|
28
|
+
else f"<{node.kind.value}>"
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
if node.kind == NodeKind.ADMONITION:
|
|
32
|
+
findings.append(
|
|
33
|
+
ConstructFinding(
|
|
34
|
+
category="Markdown",
|
|
35
|
+
construct_type="admonition",
|
|
36
|
+
file_path=doc_ir.file_path,
|
|
37
|
+
line_number=node.start_line,
|
|
38
|
+
end_line_number=node.end_line,
|
|
39
|
+
raw_snippet=snippet,
|
|
40
|
+
classification=Classification.TRANSFORM,
|
|
41
|
+
metadata={
|
|
42
|
+
"admonition_type": node.metadata.get("admonition_type"),
|
|
43
|
+
"title": node.metadata.get("title"),
|
|
44
|
+
},
|
|
45
|
+
)
|
|
46
|
+
)
|
|
47
|
+
elif node.kind == NodeKind.DETAILS_DROPDOWN:
|
|
48
|
+
findings.append(
|
|
49
|
+
ConstructFinding(
|
|
50
|
+
category="Markdown",
|
|
51
|
+
construct_type="details_dropdown",
|
|
52
|
+
file_path=doc_ir.file_path,
|
|
53
|
+
line_number=node.start_line,
|
|
54
|
+
end_line_number=node.end_line,
|
|
55
|
+
raw_snippet=snippet,
|
|
56
|
+
classification=Classification.TRANSFORM,
|
|
57
|
+
metadata={"title": node.metadata.get("title")},
|
|
58
|
+
)
|
|
59
|
+
)
|
|
60
|
+
elif node.kind == NodeKind.TAB_SET:
|
|
61
|
+
findings.append(
|
|
62
|
+
ConstructFinding(
|
|
63
|
+
category="Markdown",
|
|
64
|
+
construct_type="tab_set",
|
|
65
|
+
file_path=doc_ir.file_path,
|
|
66
|
+
line_number=node.start_line,
|
|
67
|
+
end_line_number=node.end_line,
|
|
68
|
+
raw_snippet=snippet,
|
|
69
|
+
classification=Classification.TRANSFORM,
|
|
70
|
+
metadata={"tab_count": len(node.children)},
|
|
71
|
+
)
|
|
72
|
+
)
|
|
73
|
+
elif node.kind == NodeKind.API_DIRECTIVE:
|
|
74
|
+
findings.append(
|
|
75
|
+
ConstructFinding(
|
|
76
|
+
category="API Documentation",
|
|
77
|
+
construct_type="mkdocstrings",
|
|
78
|
+
file_path=doc_ir.file_path,
|
|
79
|
+
line_number=node.start_line,
|
|
80
|
+
end_line_number=node.end_line,
|
|
81
|
+
raw_snippet=snippet,
|
|
82
|
+
classification=Classification.MANUAL,
|
|
83
|
+
metadata={"symbol": node.metadata.get("symbol")},
|
|
84
|
+
)
|
|
85
|
+
)
|
|
86
|
+
elif node.kind == NodeKind.SNIPPET_INCLUDE:
|
|
87
|
+
findings.append(
|
|
88
|
+
ConstructFinding(
|
|
89
|
+
category="Markdown",
|
|
90
|
+
construct_type="snippet_include",
|
|
91
|
+
file_path=doc_ir.file_path,
|
|
92
|
+
line_number=node.start_line,
|
|
93
|
+
end_line_number=node.end_line,
|
|
94
|
+
raw_snippet=snippet,
|
|
95
|
+
classification=Classification.TRANSFORM,
|
|
96
|
+
metadata={"filepath": node.metadata.get("filepath")},
|
|
97
|
+
)
|
|
98
|
+
)
|
|
99
|
+
elif node.kind == NodeKind.MERMAID_DIAGRAM:
|
|
100
|
+
findings.append(
|
|
101
|
+
ConstructFinding(
|
|
102
|
+
category="Markdown",
|
|
103
|
+
construct_type="mermaid_diagram",
|
|
104
|
+
file_path=doc_ir.file_path,
|
|
105
|
+
line_number=node.start_line,
|
|
106
|
+
end_line_number=node.end_line,
|
|
107
|
+
raw_snippet=snippet,
|
|
108
|
+
classification=Classification.TRANSFORM,
|
|
109
|
+
metadata={},
|
|
110
|
+
)
|
|
111
|
+
)
|
|
112
|
+
elif node.kind == NodeKind.LINK_REF:
|
|
113
|
+
href = node.metadata.get("href", "")
|
|
114
|
+
if href.endswith(".md") or ".md#" in href:
|
|
115
|
+
findings.append(
|
|
116
|
+
ConstructFinding(
|
|
117
|
+
category="Markdown",
|
|
118
|
+
construct_type="document_link",
|
|
119
|
+
file_path=doc_ir.file_path,
|
|
120
|
+
line_number=node.start_line,
|
|
121
|
+
end_line_number=node.end_line,
|
|
122
|
+
raw_snippet=href,
|
|
123
|
+
classification=Classification.PRESERVE,
|
|
124
|
+
metadata={"href": href, "text": node.metadata.get("text")},
|
|
125
|
+
)
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
# Recurse children
|
|
129
|
+
for child in node.children:
|
|
130
|
+
visit_node(child)
|
|
131
|
+
|
|
132
|
+
for root_node in doc_ir.nodes:
|
|
133
|
+
visit_node(root_node)
|
|
134
|
+
|
|
135
|
+
return findings
|
|
136
|
+
|
|
137
|
+
def analyze_directory(self, docs_dir: Path) -> Tuple[int, List[ConstructFinding]]:
|
|
138
|
+
if not docs_dir.exists():
|
|
139
|
+
return 0, []
|
|
140
|
+
|
|
141
|
+
all_findings: List[ConstructFinding] = []
|
|
142
|
+
md_files = list(docs_dir.rglob("*.md"))
|
|
143
|
+
|
|
144
|
+
for md_file in md_files:
|
|
145
|
+
file_findings = self.analyze_file(md_file)
|
|
146
|
+
all_findings.extend(file_findings)
|
|
147
|
+
|
|
148
|
+
return len(md_files), all_findings
|