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.
Files changed (43) hide show
  1. markitecture/__init__.py +41 -0
  2. markitecture/__main__.py +4 -0
  3. markitecture/cli/__init__.py +3 -0
  4. markitecture/cli/app.py +38 -0
  5. markitecture/cli/commands/__init__.py +21 -0
  6. markitecture/cli/commands/config.py +84 -0
  7. markitecture/cli/commands/links.py +146 -0
  8. markitecture/cli/commands/metrics.py +193 -0
  9. markitecture/cli/commands/mkdocs.py +39 -0
  10. markitecture/cli/commands/split.py +48 -0
  11. markitecture/errors.py +64 -0
  12. markitecture/generators/__init__.py +3 -0
  13. markitecture/generators/configs/__init__.py +0 -0
  14. markitecture/generators/configs/mintlify_json.py +0 -0
  15. markitecture/generators/configs/mkdocs_yaml.py +317 -0
  16. markitecture/metrics/__init__.py +9 -0
  17. markitecture/metrics/analyzer.py +109 -0
  18. markitecture/metrics/badges/__init__.py +28 -0
  19. markitecture/metrics/badges/base.py +7 -0
  20. markitecture/metrics/badges/compact.py +35 -0
  21. markitecture/metrics/badges/detailed.py +60 -0
  22. markitecture/metrics/badges/minimal.py +19 -0
  23. markitecture/metrics/badges/modern.py +45 -0
  24. markitecture/metrics/badges/retro.py +23 -0
  25. markitecture/metrics/badges/shields.py +124 -0
  26. markitecture/metrics/svg_generator.py +70 -0
  27. markitecture/processing/__init__.py +0 -0
  28. markitecture/processing/link_validator.py +133 -0
  29. markitecture/processing/reflink_converter.py +198 -0
  30. markitecture/processing/reflink_extractor.py +82 -0
  31. markitecture/processing/text_splitter.py +290 -0
  32. markitecture/settings/__init__.py +9 -0
  33. markitecture/settings/config.py +61 -0
  34. markitecture/settings/validators.py +26 -0
  35. markitecture/utils/__init__.py +5 -0
  36. markitecture/utils/file_handler.py +24 -0
  37. markitecture/utils/printer.py +195 -0
  38. markitecture/utils/sanitizer.py +78 -0
  39. markitecture-0.1.15.dist-info/METADATA +271 -0
  40. markitecture-0.1.15.dist-info/RECORD +43 -0
  41. markitecture-0.1.15.dist-info/WHEEL +4 -0
  42. markitecture-0.1.15.dist-info/entry_points.txt +2 -0
  43. 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,7 @@
1
+ class BaseSvgGenerator:
2
+ def __init__(self, width: int, height: int):
3
+ self.width = width
4
+ self.height = height
5
+
6
+ def render(self, content: str) -> str:
7
+ return f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {self.width} {self.height}">{content}</svg>'
@@ -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)