markitecture 0.1.15__py3-none-any.whl
Sign up to get free protection for your applications and to get access to all the features.
- markitecture/__init__.py +41 -0
- markitecture/__main__.py +4 -0
- markitecture/cli/__init__.py +3 -0
- markitecture/cli/app.py +38 -0
- markitecture/cli/commands/__init__.py +21 -0
- markitecture/cli/commands/config.py +84 -0
- markitecture/cli/commands/links.py +146 -0
- markitecture/cli/commands/metrics.py +193 -0
- markitecture/cli/commands/mkdocs.py +39 -0
- markitecture/cli/commands/split.py +48 -0
- markitecture/errors.py +64 -0
- markitecture/generators/__init__.py +3 -0
- markitecture/generators/configs/__init__.py +0 -0
- markitecture/generators/configs/mintlify_json.py +0 -0
- markitecture/generators/configs/mkdocs_yaml.py +317 -0
- markitecture/metrics/__init__.py +9 -0
- markitecture/metrics/analyzer.py +109 -0
- markitecture/metrics/badges/__init__.py +28 -0
- markitecture/metrics/badges/base.py +7 -0
- markitecture/metrics/badges/compact.py +35 -0
- markitecture/metrics/badges/detailed.py +60 -0
- markitecture/metrics/badges/minimal.py +19 -0
- markitecture/metrics/badges/modern.py +45 -0
- markitecture/metrics/badges/retro.py +23 -0
- markitecture/metrics/badges/shields.py +124 -0
- markitecture/metrics/svg_generator.py +70 -0
- markitecture/processing/__init__.py +0 -0
- markitecture/processing/link_validator.py +133 -0
- markitecture/processing/reflink_converter.py +198 -0
- markitecture/processing/reflink_extractor.py +82 -0
- markitecture/processing/text_splitter.py +290 -0
- markitecture/settings/__init__.py +9 -0
- markitecture/settings/config.py +61 -0
- markitecture/settings/validators.py +26 -0
- markitecture/utils/__init__.py +5 -0
- markitecture/utils/file_handler.py +24 -0
- markitecture/utils/printer.py +195 -0
- markitecture/utils/sanitizer.py +78 -0
- markitecture-0.1.15.dist-info/METADATA +271 -0
- markitecture-0.1.15.dist-info/RECORD +43 -0
- markitecture-0.1.15.dist-info/WHEEL +4 -0
- markitecture-0.1.15.dist-info/entry_points.txt +2 -0
- markitecture-0.1.15.dist-info/licenses/LICENSE +21 -0
File without changes
|
File without changes
|
@@ -0,0 +1,317 @@
|
|
1
|
+
import re
|
2
|
+
from pathlib import Path
|
3
|
+
from typing import (
|
4
|
+
Any,
|
5
|
+
ClassVar,
|
6
|
+
Dict,
|
7
|
+
List,
|
8
|
+
Set,
|
9
|
+
Union,
|
10
|
+
)
|
11
|
+
|
12
|
+
import yaml
|
13
|
+
|
14
|
+
from markitecture.errors import FileOperationError
|
15
|
+
from markitecture.utils.file_handler import FileHandler
|
16
|
+
|
17
|
+
|
18
|
+
class MkDocsConfig:
|
19
|
+
"""
|
20
|
+
Handles MkDocs configuration generation.
|
21
|
+
"""
|
22
|
+
|
23
|
+
# Define priority pages that should appear first in navigation
|
24
|
+
PRIORITY_PAGES: ClassVar[List[str]] = [
|
25
|
+
"readme",
|
26
|
+
"index",
|
27
|
+
"introduction",
|
28
|
+
"getting-started",
|
29
|
+
"quick-start",
|
30
|
+
]
|
31
|
+
|
32
|
+
def __init__(
|
33
|
+
self,
|
34
|
+
docs_dir: Union[str, Path] = ".markitecture/",
|
35
|
+
site_name: str = "MkDocs Site Documentation",
|
36
|
+
enable_material: bool = True,
|
37
|
+
theme_palette: Dict[str, str] | None = None,
|
38
|
+
) -> None:
|
39
|
+
if not site_name or not site_name.strip():
|
40
|
+
raise ValueError("Site name cannot be empty")
|
41
|
+
|
42
|
+
self.docs_dir = Path(docs_dir)
|
43
|
+
if not self.docs_dir.exists() and not self.docs_dir.parent.exists():
|
44
|
+
raise ValueError(f"Invalid documentation directory path: {docs_dir}")
|
45
|
+
|
46
|
+
self.site_name = site_name.strip()
|
47
|
+
self.enable_material = enable_material
|
48
|
+
self.theme_palette = theme_palette or {
|
49
|
+
"scheme": "default",
|
50
|
+
"primary": "indigo",
|
51
|
+
"accent": "indigo",
|
52
|
+
}
|
53
|
+
self.file_handler = FileHandler()
|
54
|
+
|
55
|
+
def _format_nav_title(self, filename: str) -> str:
|
56
|
+
"""Format a filename into a readable navigation title.
|
57
|
+
|
58
|
+
Args:
|
59
|
+
filename: Name of the markdown file without extension
|
60
|
+
|
61
|
+
Returns:
|
62
|
+
Formatted title suitable for navigation
|
63
|
+
"""
|
64
|
+
# Remove common prefix/suffix patterns
|
65
|
+
clean_name = re.sub(r"^[0-9]+[-_]", "", filename)
|
66
|
+
clean_name = re.sub(r"[-_]?index$", "", clean_name)
|
67
|
+
|
68
|
+
# Replace separators and capitalize
|
69
|
+
return clean_name.replace("-", " ").replace("_", " ").strip().title()
|
70
|
+
|
71
|
+
def _generate_nav(self) -> List[Dict[str, str]]:
|
72
|
+
"""Generate organized navigation structure from markdown files."""
|
73
|
+
nav: List[Dict[str, str]] = []
|
74
|
+
try:
|
75
|
+
self.docs_dir.mkdir(parents=True, exist_ok=True)
|
76
|
+
|
77
|
+
md_files = list(self.docs_dir.glob("*.md"))
|
78
|
+
if not md_files:
|
79
|
+
placeholder_path = self.docs_dir / "index.md"
|
80
|
+
placeholder_content = "# Home\n\nWelcome to your documentation!"
|
81
|
+
placeholder_path.write_text(placeholder_content, encoding="utf-8")
|
82
|
+
md_files = [placeholder_path]
|
83
|
+
|
84
|
+
# Sort files into priority and regular lists
|
85
|
+
priority_files: List[Path] = []
|
86
|
+
regular_files: List[Path] = []
|
87
|
+
for md_file in md_files:
|
88
|
+
if md_file.stem.lower() in self.PRIORITY_PAGES:
|
89
|
+
priority_files.append(md_file)
|
90
|
+
else:
|
91
|
+
regular_files.append(md_file)
|
92
|
+
|
93
|
+
# Add priority files first, in order specified in PRIORITY_PAGES
|
94
|
+
for page in self.PRIORITY_PAGES:
|
95
|
+
matching_files = [f for f in priority_files if f.stem.lower() == page]
|
96
|
+
if matching_files:
|
97
|
+
title = (
|
98
|
+
"Home"
|
99
|
+
if page in ["readme", "index"]
|
100
|
+
else self._format_nav_title(page)
|
101
|
+
)
|
102
|
+
nav.append({
|
103
|
+
title: str(matching_files[0].relative_to(self.docs_dir))
|
104
|
+
})
|
105
|
+
|
106
|
+
# Add remaining files alphabetically
|
107
|
+
for md_file in sorted(regular_files, key=lambda x: x.stem.lower()):
|
108
|
+
title = self._format_nav_title(md_file.stem)
|
109
|
+
nav.append({title: str(md_file.relative_to(self.docs_dir))})
|
110
|
+
|
111
|
+
except Exception as e:
|
112
|
+
raise FileOperationError(
|
113
|
+
f"Failed to generate navigation structure: {e!s}"
|
114
|
+
) from e
|
115
|
+
|
116
|
+
return nav
|
117
|
+
|
118
|
+
def _get_base_config(self) -> Dict[str, Union[str, Dict[str, str]]]:
|
119
|
+
"""Get comprehensive MkDocs configuration with enhanced theme settings.
|
120
|
+
|
121
|
+
Returns:
|
122
|
+
Complete MkDocs configuration dictionary
|
123
|
+
"""
|
124
|
+
config = {
|
125
|
+
"site_name": self.site_name,
|
126
|
+
"docs_dir": str(self.docs_dir),
|
127
|
+
"nav": self._generate_nav(),
|
128
|
+
}
|
129
|
+
|
130
|
+
if self.enable_material:
|
131
|
+
config.update({
|
132
|
+
"theme": {
|
133
|
+
"name": "material",
|
134
|
+
"palette": self.theme_palette,
|
135
|
+
"features": [
|
136
|
+
"navigation.instant",
|
137
|
+
"navigation.tracking",
|
138
|
+
"navigation.tabs",
|
139
|
+
"navigation.sections",
|
140
|
+
"navigation.expand",
|
141
|
+
"search.highlight",
|
142
|
+
],
|
143
|
+
},
|
144
|
+
"markdown_extensions": [
|
145
|
+
"admonition",
|
146
|
+
"pymdownx.details",
|
147
|
+
"pymdownx.superfences",
|
148
|
+
"pymdownx.highlight",
|
149
|
+
"pymdownx.inlinehilite",
|
150
|
+
"pymdownx.snippets",
|
151
|
+
"tables",
|
152
|
+
"footnotes",
|
153
|
+
],
|
154
|
+
})
|
155
|
+
|
156
|
+
return config
|
157
|
+
|
158
|
+
def generate_config(
|
159
|
+
self,
|
160
|
+
output_file: Union[str, Path] | None = None,
|
161
|
+
extra_config: Dict[str, Union[str, Dict[str, str]]] | None = None,
|
162
|
+
) -> None:
|
163
|
+
"""Generate MkDocs configuration file with enhanced error handling.
|
164
|
+
|
165
|
+
Args:
|
166
|
+
output_file: Path for configuration file output
|
167
|
+
extra_config: Additional configuration options to include
|
168
|
+
|
169
|
+
Raises:
|
170
|
+
FileOperationError: If configuration file cannot be written
|
171
|
+
ValueError: If output_file is invalid
|
172
|
+
"""
|
173
|
+
if not output_file:
|
174
|
+
output_file = self.docs_dir / "mkdocs.yml"
|
175
|
+
|
176
|
+
config = self._get_base_config()
|
177
|
+
|
178
|
+
# Merge any extra configuration settings
|
179
|
+
if extra_config:
|
180
|
+
config = self._deep_merge_configs(config, extra_config)
|
181
|
+
|
182
|
+
try:
|
183
|
+
# Ensure parent directories exist
|
184
|
+
output_path = Path(output_file)
|
185
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
186
|
+
|
187
|
+
with output_path.open("w", encoding="utf-8") as file:
|
188
|
+
yaml.dump(
|
189
|
+
config,
|
190
|
+
file,
|
191
|
+
Dumper=SpacedDumper,
|
192
|
+
default_flow_style=False,
|
193
|
+
sort_keys=False,
|
194
|
+
allow_unicode=True,
|
195
|
+
indent=2, # Ensure consistent indentation
|
196
|
+
)
|
197
|
+
|
198
|
+
except Exception as e:
|
199
|
+
raise FileOperationError(
|
200
|
+
f"Failed to write MkDocs configuration to {output_file}: {e!s}"
|
201
|
+
) from e
|
202
|
+
|
203
|
+
def _deep_merge_configs(
|
204
|
+
self, base: dict[str, Any], update: dict[str, Any]
|
205
|
+
) -> dict[str, Any]:
|
206
|
+
"""Recursively merge two configuration dictionaries.
|
207
|
+
|
208
|
+
Args:
|
209
|
+
base: Base configuration dictionary
|
210
|
+
update: Update configuration dictionary
|
211
|
+
|
212
|
+
Returns:
|
213
|
+
Merged configuration dictionary
|
214
|
+
"""
|
215
|
+
result = base.copy()
|
216
|
+
for key, value in update.items():
|
217
|
+
if (
|
218
|
+
isinstance(value, dict)
|
219
|
+
and key in result
|
220
|
+
and isinstance(result[key], dict)
|
221
|
+
):
|
222
|
+
result[key] = self._deep_merge_configs(result[key], value)
|
223
|
+
else:
|
224
|
+
result[key] = value
|
225
|
+
return result
|
226
|
+
|
227
|
+
def update_nav(self, output_file: Union[str, Path] = "mkdocs.yml") -> None:
|
228
|
+
"""Update navigation while preserving other configuration settings.
|
229
|
+
|
230
|
+
Args:
|
231
|
+
output_file: Path to existing MkDocs configuration file
|
232
|
+
|
233
|
+
Raises:
|
234
|
+
FileOperationError: If config file cannot be accessed
|
235
|
+
FileNotFoundError: If config file does not exist
|
236
|
+
"""
|
237
|
+
output_path = Path(output_file)
|
238
|
+
if not output_path.exists():
|
239
|
+
raise FileNotFoundError(f"Config file not found: {output_file}")
|
240
|
+
|
241
|
+
try:
|
242
|
+
with output_path.open("r", encoding="utf-8") as f:
|
243
|
+
config = yaml.safe_load(f)
|
244
|
+
|
245
|
+
# Update navigation structure
|
246
|
+
config["nav"] = self._generate_nav()
|
247
|
+
|
248
|
+
# Write updated configuration
|
249
|
+
with output_path.open("w", encoding="utf-8") as file:
|
250
|
+
yaml.dump(
|
251
|
+
config,
|
252
|
+
file,
|
253
|
+
Dumper=SpacedDumper,
|
254
|
+
default_flow_style=False,
|
255
|
+
sort_keys=False,
|
256
|
+
allow_unicode=True,
|
257
|
+
indent=2, # Ensure consistent indentation
|
258
|
+
)
|
259
|
+
|
260
|
+
except Exception as e:
|
261
|
+
raise FileOperationError(
|
262
|
+
f"Failed to update navigation in {output_file}: {e!s}"
|
263
|
+
) from e
|
264
|
+
|
265
|
+
|
266
|
+
class SpacedDumper(yaml.Dumper):
|
267
|
+
"""
|
268
|
+
Custom YAML dumper that adds spacing between major configuration sections.
|
269
|
+
|
270
|
+
This dumper ensures that the generated YAML config is more readable by:
|
271
|
+
- Keeping basic settings grouped together at the top
|
272
|
+
- Adding line breaks between major sections like nav, theme, and extensions
|
273
|
+
- Maintaining proper indentation throughout
|
274
|
+
"""
|
275
|
+
|
276
|
+
# Settings that should be grouped together at the top without extra spacing
|
277
|
+
BASIC_SETTINGS: ClassVar[Set[str]] = {
|
278
|
+
"site_name",
|
279
|
+
"docs_dir",
|
280
|
+
"site_url",
|
281
|
+
"repo_url",
|
282
|
+
"repo_name",
|
283
|
+
}
|
284
|
+
|
285
|
+
def __init__(self, *args: Any, **kwargs: Any):
|
286
|
+
super().__init__(*args, **kwargs)
|
287
|
+
self.last_was_basic = False
|
288
|
+
self.current_key: str | None = None
|
289
|
+
|
290
|
+
def represent_mapping(
|
291
|
+
self,
|
292
|
+
tag: str,
|
293
|
+
mapping: Any, # Union[SupportsItems[Any, Any], Iterable[tuple[Any, Any]]],
|
294
|
+
flow_style: bool | None = None,
|
295
|
+
) -> yaml.MappingNode:
|
296
|
+
"""Override to track the current key being processed."""
|
297
|
+
# Get the key if we're at the top level
|
298
|
+
if len(self.indents) == 0 and mapping:
|
299
|
+
self.current_key = next(iter(mapping)) # type: ignore
|
300
|
+
return super().represent_mapping(tag, mapping, flow_style)
|
301
|
+
|
302
|
+
def write_line_break(self, data: str | None = None) -> None:
|
303
|
+
"""Add extra line breaks between major sections, but not basic settings."""
|
304
|
+
super().write_line_break(data)
|
305
|
+
|
306
|
+
# Only add extra spacing for top-level items that aren't basic settings
|
307
|
+
if (
|
308
|
+
len(self.indents) == 1
|
309
|
+
and self.current_key not in self.BASIC_SETTINGS
|
310
|
+
and not self.last_was_basic
|
311
|
+
):
|
312
|
+
super().write_line_break()
|
313
|
+
|
314
|
+
# Track whether we just processed a basic setting
|
315
|
+
self.last_was_basic = (
|
316
|
+
len(self.indents) == 1 and self.current_key in self.BASIC_SETTINGS
|
317
|
+
)
|
@@ -0,0 +1,9 @@
|
|
1
|
+
from markitecture.metrics.analyzer import ReadabilityAnalyzer, ReadabilityMetrics
|
2
|
+
from markitecture.metrics.svg_generator import BadgeStyle, MetricsSvgGenerator
|
3
|
+
|
4
|
+
__all__ = [
|
5
|
+
"BadgeStyle",
|
6
|
+
"MetricsSvgGenerator",
|
7
|
+
"ReadabilityAnalyzer",
|
8
|
+
"ReadabilityMetrics",
|
9
|
+
]
|
@@ -0,0 +1,109 @@
|
|
1
|
+
import re
|
2
|
+
from dataclasses import dataclass
|
3
|
+
from typing import Tuple
|
4
|
+
|
5
|
+
|
6
|
+
@dataclass
|
7
|
+
class ReadabilityMetrics:
|
8
|
+
"""
|
9
|
+
Data class to store readability metrics.
|
10
|
+
"""
|
11
|
+
|
12
|
+
word_count: int
|
13
|
+
sentence_count: int
|
14
|
+
avg_words_per_sentence: float
|
15
|
+
reading_time_mins: float
|
16
|
+
complexity_score: float
|
17
|
+
heading_count: int
|
18
|
+
code_block_count: int
|
19
|
+
link_count: int
|
20
|
+
image_count: int
|
21
|
+
|
22
|
+
|
23
|
+
class ReadabilityAnalyzer:
|
24
|
+
"""
|
25
|
+
Analyze the content and calculate readability metrics.
|
26
|
+
"""
|
27
|
+
|
28
|
+
def __init__(self) -> None:
|
29
|
+
self.READING_SPEED = 238 # words per minute
|
30
|
+
self.CODE_BLOCK_TIME = 20 # seconds
|
31
|
+
self.WEIGHTS = {
|
32
|
+
"avg_sentence_length": 0.3,
|
33
|
+
"code_density": 0.3,
|
34
|
+
"heading_depth": 0.2,
|
35
|
+
"link_density": 0.1,
|
36
|
+
"image_density": 0.1,
|
37
|
+
}
|
38
|
+
|
39
|
+
def _count_words(self, text: str) -> int:
|
40
|
+
"""Count words in the text after removing code blocks and URLs."""
|
41
|
+
text = re.sub(r"```.*?```", "", text, flags=re.DOTALL)
|
42
|
+
text = re.sub(r"`.*?`", "", text)
|
43
|
+
text = re.sub(r"http[s]?://\S+", "", text)
|
44
|
+
words = re.findall(r"\w+", text)
|
45
|
+
return len(words)
|
46
|
+
|
47
|
+
def _count_sentences(self, text: str) -> int:
|
48
|
+
"""Split text into sentences and count them."""
|
49
|
+
text = re.sub(r"```.*?```", "", text, flags=re.DOTALL)
|
50
|
+
sentences = re.split(r"[.!?]+", text)
|
51
|
+
return len([s for s in sentences if s.strip()])
|
52
|
+
|
53
|
+
def _count_code_blocks(self, text: str) -> int:
|
54
|
+
"""Count code blocks enclosed in triple backticks."""
|
55
|
+
return len(re.findall(r"```.*?```", text, re.DOTALL))
|
56
|
+
|
57
|
+
def _count_headings(self, text: str) -> Tuple[int, float]:
|
58
|
+
"""Calculate average heading depth."""
|
59
|
+
headings = re.findall(r"^(#{1,6})\s+.*$", text, re.MULTILINE)
|
60
|
+
if not headings:
|
61
|
+
return 0, 0.0
|
62
|
+
total_depth = sum(len(h) for h in headings)
|
63
|
+
return len(headings), total_depth / len(headings)
|
64
|
+
|
65
|
+
def _count_links_and_images(self, text: str) -> Tuple[int, int]:
|
66
|
+
"""Count links only if not preceded by an exclamation mark."""
|
67
|
+
links = len(re.findall(r"(?<!\!)\[(.*?)\]\((.*?)\)", text))
|
68
|
+
images = len(re.findall(r"!\[(.*?)\]\((.*?)\)", text))
|
69
|
+
return links, images
|
70
|
+
|
71
|
+
def calculate_complexity(self, metrics: ReadabilityMetrics) -> float:
|
72
|
+
"""Calculate a weighted complexity score based on various metrics."""
|
73
|
+
scores = {
|
74
|
+
"avg_sentence_length": min(metrics.avg_words_per_sentence / 20.0, 1.0),
|
75
|
+
"code_density": metrics.code_block_count / max(metrics.word_count / 500, 1),
|
76
|
+
"heading_depth": metrics.heading_count / max(metrics.word_count / 200, 1),
|
77
|
+
"link_density": metrics.link_count / max(metrics.word_count / 200, 1),
|
78
|
+
"image_density": metrics.image_count / max(metrics.word_count / 300, 1),
|
79
|
+
}
|
80
|
+
weighted_score = sum(
|
81
|
+
scores[metric] * weight for metric, weight in self.WEIGHTS.items()
|
82
|
+
)
|
83
|
+
return min(round(weighted_score * 100), 100)
|
84
|
+
|
85
|
+
def analyze_document(self, content: str) -> ReadabilityMetrics:
|
86
|
+
"""Analyze the content and calculate readability metrics."""
|
87
|
+
word_count = self._count_words(content)
|
88
|
+
sentence_count = self._count_sentences(content)
|
89
|
+
code_blocks = self._count_code_blocks(content)
|
90
|
+
heading_count, _ = self._count_headings(content)
|
91
|
+
link_count, image_count = self._count_links_and_images(content)
|
92
|
+
avg_words = word_count / max(sentence_count, 1) if sentence_count > 0 else 0
|
93
|
+
base_reading_time = word_count / self.READING_SPEED
|
94
|
+
code_reading_time = (code_blocks * self.CODE_BLOCK_TIME) / 60
|
95
|
+
total_reading_time = base_reading_time + code_reading_time
|
96
|
+
|
97
|
+
metrics = ReadabilityMetrics(
|
98
|
+
word_count=word_count,
|
99
|
+
sentence_count=sentence_count,
|
100
|
+
avg_words_per_sentence=round(avg_words, 1),
|
101
|
+
reading_time_mins=round(total_reading_time, 1),
|
102
|
+
complexity_score=0, # placeholder
|
103
|
+
heading_count=heading_count,
|
104
|
+
code_block_count=code_blocks,
|
105
|
+
link_count=link_count,
|
106
|
+
image_count=image_count,
|
107
|
+
)
|
108
|
+
metrics.complexity_score = self.calculate_complexity(metrics)
|
109
|
+
return metrics
|
@@ -0,0 +1,28 @@
|
|
1
|
+
from enum import StrEnum
|
2
|
+
|
3
|
+
from markitecture.metrics.badges.compact import CompactBadgeGenerator
|
4
|
+
from markitecture.metrics.badges.detailed import DetailedBadgeGenerator
|
5
|
+
from markitecture.metrics.badges.minimal import MinimalBadgeGenerator
|
6
|
+
from markitecture.metrics.badges.modern import ModernBadgeGenerator
|
7
|
+
from markitecture.metrics.badges.retro import RetroBadgeGenerator
|
8
|
+
from markitecture.metrics.badges.shields import ShieldsBadgeGenerator
|
9
|
+
|
10
|
+
|
11
|
+
class BadgeStyle(StrEnum):
|
12
|
+
MODERN = "modern"
|
13
|
+
COMPACT = "compact"
|
14
|
+
DETAILED = "detailed"
|
15
|
+
MINIMAL = "minimal"
|
16
|
+
RETRO = "retro"
|
17
|
+
SHIELDS = "shields"
|
18
|
+
|
19
|
+
|
20
|
+
__all__ = [
|
21
|
+
"BadgeStyle",
|
22
|
+
"CompactBadgeGenerator",
|
23
|
+
"DetailedBadgeGenerator",
|
24
|
+
"MinimalBadgeGenerator",
|
25
|
+
"ModernBadgeGenerator",
|
26
|
+
"RetroBadgeGenerator",
|
27
|
+
"ShieldsBadgeGenerator",
|
28
|
+
]
|
@@ -0,0 +1,35 @@
|
|
1
|
+
from typing import Tuple
|
2
|
+
|
3
|
+
from markitecture.metrics.analyzer import ReadabilityMetrics
|
4
|
+
from markitecture.metrics.badges.base import BaseSvgGenerator
|
5
|
+
|
6
|
+
|
7
|
+
class CompactBadgeGenerator(BaseSvgGenerator):
|
8
|
+
def __init__(
|
9
|
+
self,
|
10
|
+
width: int = 400,
|
11
|
+
height: int = 40,
|
12
|
+
gradient: Tuple[str, str] = ("#7934C5", "#4158D0"),
|
13
|
+
):
|
14
|
+
super().__init__(width, height)
|
15
|
+
self.gradient = gradient
|
16
|
+
|
17
|
+
def generate(self, metrics: ReadabilityMetrics) -> str:
|
18
|
+
gradient_def = (
|
19
|
+
f"<defs>\n"
|
20
|
+
f" <linearGradient id='gradientBg' x1='0%' y1='0%' x2='100%' y2='0%'>\n"
|
21
|
+
f" <stop offset='0%' style='stop-color:{self.gradient[0]}' />\n"
|
22
|
+
f" <stop offset='100%' style='stop-color:{self.gradient[1]}' />\n"
|
23
|
+
f" </linearGradient>\n"
|
24
|
+
f"</defs>\n"
|
25
|
+
)
|
26
|
+
content = (
|
27
|
+
f"<!-- Compact badge content -->\n"
|
28
|
+
f"<rect x='0' y='0' width='{self.width}' height='{self.height}' rx='20' fill='white' "
|
29
|
+
f"stroke='url(#gradientBg)' stroke-width='2' />\n"
|
30
|
+
f"<text x='20' y='{self.height / 2}' font-family='Arial, sans-serif' font-size='14' fill='#666' "
|
31
|
+
f"dominant-baseline='middle'>"
|
32
|
+
f"{metrics.reading_time_mins}m read • {metrics.word_count:,} words • {metrics.complexity_score}% complexity"
|
33
|
+
f"</text>\n"
|
34
|
+
)
|
35
|
+
return self.render(gradient_def + content)
|
@@ -0,0 +1,60 @@
|
|
1
|
+
from typing import Tuple
|
2
|
+
|
3
|
+
from markitecture.metrics.analyzer import ReadabilityMetrics
|
4
|
+
from markitecture.metrics.badges.base import BaseSvgGenerator
|
5
|
+
|
6
|
+
|
7
|
+
class DetailedBadgeGenerator(BaseSvgGenerator):
|
8
|
+
def __init__(
|
9
|
+
self,
|
10
|
+
width: int = 600,
|
11
|
+
height: int = 200,
|
12
|
+
gradient: Tuple[str, str] = ("#7934C5", "#4158D0"),
|
13
|
+
):
|
14
|
+
super().__init__(width, height)
|
15
|
+
self.gradient = gradient
|
16
|
+
|
17
|
+
def generate(self, metrics: ReadabilityMetrics) -> str:
|
18
|
+
gradient_def = (
|
19
|
+
f"<defs>\n"
|
20
|
+
f" <linearGradient id='gradientBg' x1='0%' y1='0%' x2='100%' y2='0%'>\n"
|
21
|
+
f" <stop offset='0%' style='stop-color:{self.gradient[0]}' />\n"
|
22
|
+
f" <stop offset='100%' style='stop-color:{self.gradient[1]}' />\n"
|
23
|
+
f" </linearGradient>\n"
|
24
|
+
f" <filter id='shadow' x='-20%' y='-20%' width='140%' height='140%'>\n"
|
25
|
+
f" <feGaussianBlur in='SourceAlpha' stdDeviation='2' />\n"
|
26
|
+
f" <feOffset dx='2' dy='2' />\n"
|
27
|
+
f" <feComponentTransfer>\n"
|
28
|
+
f" <feFuncA type='linear' slope='0.2' />\n"
|
29
|
+
f" </feComponentTransfer>\n"
|
30
|
+
f" <feMerge>\n"
|
31
|
+
f" <feMergeNode />\n"
|
32
|
+
f" <feMergeNode in='SourceGraphic' />\n"
|
33
|
+
f" </feMerge>\n"
|
34
|
+
f" </filter>\n"
|
35
|
+
f"</defs>\n"
|
36
|
+
)
|
37
|
+
content = (
|
38
|
+
f"<!-- Detailed badge content -->\n"
|
39
|
+
f"<rect x='0' y='0' width='{self.width}' height='{self.height}' rx='15' fill='white' "
|
40
|
+
f"stroke='url(#gradientBg)' stroke-width='2' filter='url(#shadow)' />\n"
|
41
|
+
f"<text x='30' y='40' font-family='Arial, sans-serif' font-size='20' font-weight='bold' "
|
42
|
+
f"fill='url(#gradientBg)'>Document Analytics</text>\n"
|
43
|
+
f"<text x='30' y='80' font-family='Arial, sans-serif' font-size='14' fill='#666'>"
|
44
|
+
f"Reading Time: {metrics.reading_time_mins} minutes</text>\n"
|
45
|
+
f"<text x='30' y='105' font-family='Arial, sans-serif' font-size='14' fill='#666'>"
|
46
|
+
f"Word Count: {metrics.word_count:,}</text>\n"
|
47
|
+
f"<text x='30' y='130' font-family='Arial, sans-serif' font-size='14' fill='#666'>"
|
48
|
+
f"Avg Words/Sentence: {metrics.avg_words_per_sentence:.1f}</text>\n"
|
49
|
+
f"<text x='{self.width / 2 + 30}' y='80' font-family='Arial, sans-serif' font-size='14' fill='#666'>"
|
50
|
+
f"Headings: {metrics.heading_count}</text>\n"
|
51
|
+
f"<text x='{self.width / 2 + 30}' y='105' font-family='Arial, sans-serif' font-size='14' fill='#666'>"
|
52
|
+
f"Code Blocks: {metrics.code_block_count}</text>\n"
|
53
|
+
f"<text x='{self.width / 2 + 30}' y='130' font-family='Arial, sans-serif' font-size='14' fill='#666'>"
|
54
|
+
f"Links: {metrics.link_count}</text>\n"
|
55
|
+
f"<rect x='30' y='160' width='540' height='8' rx='4' fill='#eee' />\n"
|
56
|
+
f"<rect x='30' y='160' width='{5.4 * metrics.complexity_score}' height='8' rx='4' fill='url(#gradientBg)' />\n"
|
57
|
+
f"<text x='{self.width - 40}' y='170' font-family='Arial, sans-serif' font-size='12' fill='#666' text-anchor='end'>"
|
58
|
+
f"{metrics.complexity_score}% Complexity</text>\n"
|
59
|
+
)
|
60
|
+
return self.render(gradient_def + content)
|
@@ -0,0 +1,19 @@
|
|
1
|
+
from markitecture.metrics.analyzer import ReadabilityMetrics
|
2
|
+
from markitecture.metrics.badges.base import BaseSvgGenerator
|
3
|
+
|
4
|
+
|
5
|
+
class MinimalBadgeGenerator(BaseSvgGenerator):
|
6
|
+
def __init__(self, width: int = 300, height: int = 80, color: str = "#7934C5"):
|
7
|
+
super().__init__(width, height)
|
8
|
+
self.color = color
|
9
|
+
|
10
|
+
def generate(self, metrics: ReadabilityMetrics) -> str:
|
11
|
+
content = (
|
12
|
+
f"<!-- Minimal badge content -->\n"
|
13
|
+
f"<rect x='0' y='0' width='{self.width}' height='{self.height}' fill='white' />\n"
|
14
|
+
f"<text x='20' y='30' font-family='Arial, sans-serif' font-size='16' fill='#333'>"
|
15
|
+
f"{metrics.reading_time_mins} min read</text>\n"
|
16
|
+
f"<text x='20' y='55' font-family='Arial, sans-serif' font-size='14' fill='{self.color}'>"
|
17
|
+
f"{metrics.complexity_score}% complexity</text>\n"
|
18
|
+
)
|
19
|
+
return self.render(content)
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# markitecture/metrics/badges/modern.py
|
2
|
+
from typing import Tuple
|
3
|
+
|
4
|
+
from markitecture.metrics.analyzer import ReadabilityMetrics
|
5
|
+
from markitecture.metrics.badges.base import BaseSvgGenerator
|
6
|
+
|
7
|
+
|
8
|
+
class ModernBadgeGenerator(BaseSvgGenerator):
|
9
|
+
def __init__(
|
10
|
+
self,
|
11
|
+
width: int = 560,
|
12
|
+
height: int = 140,
|
13
|
+
gradient: Tuple[str, str] = ("#7934C5", "#4158D0"),
|
14
|
+
):
|
15
|
+
super().__init__(width, height)
|
16
|
+
self.gradient = gradient
|
17
|
+
|
18
|
+
def generate(self, metrics: ReadabilityMetrics) -> str:
|
19
|
+
# Improved gradient with additional stops for a smoother transition.
|
20
|
+
gradient_def = (
|
21
|
+
f"<defs>\n"
|
22
|
+
f" <linearGradient id='gradientBg' x1='0%' y1='0%' x2='100%' y2='0%'>\n"
|
23
|
+
f" <stop offset='0%' style='stop-color:{self.gradient[0]}; stop-opacity:1' />\n"
|
24
|
+
f" <stop offset='50%' style='stop-color:#6A2C70; stop-opacity:0.8' />\n"
|
25
|
+
f" <stop offset='100%' style='stop-color:{self.gradient[1]}; stop-opacity:1' />\n"
|
26
|
+
f" </linearGradient>\n"
|
27
|
+
f" <filter id='dropShadow' x='-10%' y='-10%' width='120%' height='120%'>\n"
|
28
|
+
f" <feGaussianBlur in='SourceAlpha' stdDeviation='3' />\n"
|
29
|
+
f" <feOffset dx='3' dy='3' result='offsetblur'/>\n"
|
30
|
+
f" <feMerge>\n"
|
31
|
+
f" <feMergeNode/>\n"
|
32
|
+
f" <feMergeNode in='SourceGraphic'/>\n"
|
33
|
+
f" </feMerge>\n"
|
34
|
+
f" </filter>\n"
|
35
|
+
f"</defs>\n"
|
36
|
+
)
|
37
|
+
content = (
|
38
|
+
f"<!-- Improved Modern badge content -->\n"
|
39
|
+
f"<rect x='0' y='0' width='{self.width}' height='{self.height}' fill='url(#gradientBg)' filter='url(#dropShadow)' rx='15'/>\n"
|
40
|
+
f"<text x='30' y='{self.height / 2 - 10}' font-family='Helvetica, Arial, sans-serif' font-size='24' fill='white' font-weight='bold'>"
|
41
|
+
f"{metrics.reading_time_mins} min read</text>\n"
|
42
|
+
f"<text x='30' y='{self.height / 2 + 20}' font-family='Helvetica, Arial, sans-serif' font-size='16' fill='white'>"
|
43
|
+
f"{metrics.word_count:,} words | {metrics.complexity_score}% complexity</text>\n"
|
44
|
+
)
|
45
|
+
return self.render(gradient_def + content)
|
@@ -0,0 +1,23 @@
|
|
1
|
+
from markitecture.metrics.analyzer import ReadabilityMetrics
|
2
|
+
from markitecture.metrics.badges.base import BaseSvgGenerator
|
3
|
+
|
4
|
+
|
5
|
+
class RetroBadgeGenerator(BaseSvgGenerator):
|
6
|
+
def __init__(self, width: int = 480, height: int = 120):
|
7
|
+
super().__init__(width, height)
|
8
|
+
|
9
|
+
def generate(self, metrics: ReadabilityMetrics) -> str:
|
10
|
+
content = (
|
11
|
+
f"<!-- Retro badge content -->\n"
|
12
|
+
f"<rect x='4' y='4' width='{self.width - 8}' height='{self.height - 8}' fill='white' stroke='#333' stroke-width='2' />\n"
|
13
|
+
f"<rect x='0' y='0' width='4' height='4' fill='#333' />\n"
|
14
|
+
f"<rect x='{self.width - 4}' y='0' width='4' height='4' fill='#333' />\n"
|
15
|
+
f"<rect x='0' y='{self.height - 4}' width='4' height='4' fill='#333' />\n"
|
16
|
+
f"<rect x='{self.width - 4}' y='{self.height - 4}' width='4' height='4' fill='#333' />\n"
|
17
|
+
f"<text x='{self.width / 2}' y='40' font-family='Courier, monospace' font-size='16' fill='#333' text-anchor='middle'>DOCUMENT METRICS</text>\n"
|
18
|
+
f"<text x='{self.width / 2}' y='70' font-family='Courier, monospace' font-size='14' fill='#666' text-anchor='middle'>"
|
19
|
+
f"{metrics.reading_time_mins}m | {metrics.word_count:,} words | {metrics.complexity_score}% comp.</text>\n"
|
20
|
+
f"<text x='{self.width / 2}' y='95' font-family='Courier, monospace' font-size='12' fill='#999' text-anchor='middle'>"
|
21
|
+
f"H:{metrics.heading_count} C:{metrics.code_block_count} L:{metrics.link_count} I:{metrics.image_count}</text>\n"
|
22
|
+
)
|
23
|
+
return self.render(content)
|