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.
- 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)
|