markitecture 0.1.15__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.
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)