htmlgen-mcp 0.2.0__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.
Potentially problematic release.
This version of htmlgen-mcp might be problematic. Click here for more details.
- MCP/__init__.py +6 -0
- MCP/web_agent_server.py +1257 -0
- agents/__init__.py +6 -0
- agents/smart_web_agent.py +2384 -0
- agents/web_tools/__init__.py +84 -0
- agents/web_tools/bootstrap.py +49 -0
- agents/web_tools/browser.py +28 -0
- agents/web_tools/colors.py +137 -0
- agents/web_tools/css.py +1473 -0
- agents/web_tools/edgeone_deploy.py +541 -0
- agents/web_tools/html_templates.py +1770 -0
- agents/web_tools/images.py +600 -0
- agents/web_tools/images_fixed.py +195 -0
- agents/web_tools/js.py +235 -0
- agents/web_tools/navigation.py +386 -0
- agents/web_tools/project.py +34 -0
- agents/web_tools/simple_builder.py +346 -0
- agents/web_tools/simple_css.py +475 -0
- agents/web_tools/simple_js.py +454 -0
- agents/web_tools/simple_templates.py +220 -0
- agents/web_tools/validation.py +65 -0
- htmlgen_mcp-0.2.0.dist-info/METADATA +171 -0
- htmlgen_mcp-0.2.0.dist-info/RECORD +26 -0
- htmlgen_mcp-0.2.0.dist-info/WHEEL +5 -0
- htmlgen_mcp-0.2.0.dist-info/entry_points.txt +2 -0
- htmlgen_mcp-0.2.0.dist-info/top_level.txt +2 -0
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
"""导航栏生成工具"""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import html
|
|
5
|
+
import os
|
|
6
|
+
import re
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def create_responsive_navbar(file_path: str, brand_name: str = "公司名称", nav_items: list = None, cta: dict | None = None, theme: dict | None = None):
|
|
11
|
+
"""创建响应式导航栏组件
|
|
12
|
+
|
|
13
|
+
参数说明:
|
|
14
|
+
- file_path: 目标HTML文件路径;若非 .html 则会创建为独立组件片段。
|
|
15
|
+
- brand_name: 品牌名称(导航左侧)。
|
|
16
|
+
- nav_items: 导航项列表。标准结构为:
|
|
17
|
+
[{"name": "首页", "href": "#home", "active": true}]
|
|
18
|
+
为增强容错,也兼容以下别名键:
|
|
19
|
+
• 名称:name | text | title | label
|
|
20
|
+
• 链接:href | url | link
|
|
21
|
+
active 缺省时默认第一个为 True,其他为 False。
|
|
22
|
+
- cta: 可选字典,用于自定义右侧按钮。可包含键:
|
|
23
|
+
text / short_text / href / icon / style / size_class / extra_class / visible。
|
|
24
|
+
当不给出或缺少字段时,会结合导航项自动推导合适的 CTA(如"联系我们""查看菜单"等)。
|
|
25
|
+
- theme: 可选字典,用于自定义导航栏主题。可包含键:
|
|
26
|
+
primary_color / secondary_color / text_color / bg_opacity / use_gradient / custom_class。
|
|
27
|
+
例如:{"primary_color": "#8B4513", "text_color": "white", "use_gradient": false}
|
|
28
|
+
"""
|
|
29
|
+
# 处理主题配置
|
|
30
|
+
theme = theme or {}
|
|
31
|
+
primary_color = theme.get("primary_color", "#0d6efd")
|
|
32
|
+
secondary_color = theme.get("secondary_color", "#6610f2")
|
|
33
|
+
text_color = theme.get("text_color", "white")
|
|
34
|
+
bg_opacity = theme.get("bg_opacity", "0.95")
|
|
35
|
+
use_gradient = theme.get("use_gradient", True)
|
|
36
|
+
custom_class = theme.get("custom_class", "navbar-glass")
|
|
37
|
+
|
|
38
|
+
# 根据主题生成导航栏背景样式
|
|
39
|
+
if use_gradient:
|
|
40
|
+
navbar_bg_style = f"background: linear-gradient(135deg, {primary_color} 0%, {secondary_color} 100%);"
|
|
41
|
+
else:
|
|
42
|
+
# 如果primary_color是RGB格式,转换为rgba
|
|
43
|
+
if primary_color.startswith("#"):
|
|
44
|
+
# 简单的hex到rgba转换
|
|
45
|
+
navbar_bg_style = f"background: {primary_color}; opacity: {bg_opacity};"
|
|
46
|
+
elif primary_color.startswith("rgb"):
|
|
47
|
+
# 将rgb转换为rgba
|
|
48
|
+
navbar_bg_style = f"background: {primary_color.replace('rgb', 'rgba').replace(')', f', {bg_opacity})')};"
|
|
49
|
+
else:
|
|
50
|
+
navbar_bg_style = f"background: rgba({primary_color}, {bg_opacity});"
|
|
51
|
+
|
|
52
|
+
navbar_bg_style += " backdrop-filter: blur(10px);"
|
|
53
|
+
|
|
54
|
+
# 默认导航项
|
|
55
|
+
if nav_items is None:
|
|
56
|
+
nav_items = [
|
|
57
|
+
{"name": "首页", "href": "#home", "active": True},
|
|
58
|
+
{"name": "服务", "href": "#services", "active": False},
|
|
59
|
+
{"name": "联系", "href": "#contact", "active": False}
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
# 归一化单个导航项,避免 KeyError
|
|
63
|
+
def _normalize_item(raw: object, index: int) -> tuple[dict, bool]:
|
|
64
|
+
explicit_active = False
|
|
65
|
+
if not isinstance(raw, dict):
|
|
66
|
+
return {"name": str(raw), "href": "#", "active": index == 0}, False
|
|
67
|
+
name = (
|
|
68
|
+
raw.get("name")
|
|
69
|
+
or raw.get("text")
|
|
70
|
+
or raw.get("title")
|
|
71
|
+
or raw.get("label")
|
|
72
|
+
)
|
|
73
|
+
href = (
|
|
74
|
+
raw.get("href")
|
|
75
|
+
or raw.get("url")
|
|
76
|
+
or raw.get("link")
|
|
77
|
+
)
|
|
78
|
+
explicit_active = "active" in raw
|
|
79
|
+
active = raw.get("active")
|
|
80
|
+
if not name:
|
|
81
|
+
name = f"导航{index + 1}"
|
|
82
|
+
if not href:
|
|
83
|
+
# 尝试基于名称生成一个简易锚点(仅作为兜底)
|
|
84
|
+
base = ''.join(ch.lower() if ch.isalnum() else '-' for ch in str(name))
|
|
85
|
+
base = base.strip('-') or 'section'
|
|
86
|
+
href = f"#{base}"
|
|
87
|
+
if active is None:
|
|
88
|
+
active = index == 0
|
|
89
|
+
return {"name": name, "href": href, "active": bool(active)}, explicit_active
|
|
90
|
+
|
|
91
|
+
normalized_items: list[dict] = []
|
|
92
|
+
explicit_flags: list[bool] = []
|
|
93
|
+
for i, item in enumerate(nav_items or []):
|
|
94
|
+
normalized, explicit = _normalize_item(item, i)
|
|
95
|
+
normalized_items.append(normalized)
|
|
96
|
+
explicit_flags.append(explicit)
|
|
97
|
+
|
|
98
|
+
# 基于当前文件自动高亮对应导航项(仅在未显式指定 active 时处理)
|
|
99
|
+
current_page = os.path.basename(file_path).lower()
|
|
100
|
+
if not current_page.endswith(".html"):
|
|
101
|
+
current_page = ""
|
|
102
|
+
|
|
103
|
+
highlighted = False
|
|
104
|
+
for idx, item in enumerate(normalized_items):
|
|
105
|
+
if explicit_flags[idx]:
|
|
106
|
+
highlighted = highlighted or item.get("active", False)
|
|
107
|
+
continue
|
|
108
|
+
|
|
109
|
+
href = (item.get("href") or "").lower()
|
|
110
|
+
match_current = False
|
|
111
|
+
if current_page:
|
|
112
|
+
if href.endswith(current_page):
|
|
113
|
+
match_current = True
|
|
114
|
+
elif current_page == "index.html" and href in {"index.html", "./", "#", "#home"}:
|
|
115
|
+
match_current = True
|
|
116
|
+
|
|
117
|
+
if match_current:
|
|
118
|
+
item["active"] = True
|
|
119
|
+
highlighted = True
|
|
120
|
+
else:
|
|
121
|
+
item["active"] = False
|
|
122
|
+
|
|
123
|
+
if not highlighted and normalized_items:
|
|
124
|
+
# 若仍未有激活项,默认第一个为 active
|
|
125
|
+
normalized_items[0]["active"] = True
|
|
126
|
+
|
|
127
|
+
# 生成导航项HTML
|
|
128
|
+
nav_items_html = ""
|
|
129
|
+
for item in normalized_items:
|
|
130
|
+
active_class = " active" if item.get("active", False) else ""
|
|
131
|
+
nav_items_html += (
|
|
132
|
+
f' <li class="nav-item"><a class="nav-link{active_class}" '
|
|
133
|
+
f'href="{item["href"]}" style="color: {text_color};">{item["name"]}</a></li>\n'
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
# 品牌链接:优先跳转到第一个导航项(通常为首页),避免在子页上指向不存在的 #home
|
|
137
|
+
brand_href = normalized_items[0]["href"] if normalized_items else "#home"
|
|
138
|
+
|
|
139
|
+
def _find_item(keywords: tuple[str, ...]) -> dict | None:
|
|
140
|
+
for item in normalized_items:
|
|
141
|
+
name = (item.get("name") or "").lower()
|
|
142
|
+
href = (item.get("href") or "").lower()
|
|
143
|
+
if any(key in name for key in keywords) or any(key in href for key in keywords):
|
|
144
|
+
return item
|
|
145
|
+
return None
|
|
146
|
+
|
|
147
|
+
def _derive_cta(raw_cta: dict | None) -> dict:
|
|
148
|
+
# 根据主题决定CTA按钮的默认样式
|
|
149
|
+
if use_gradient:
|
|
150
|
+
style_default = (
|
|
151
|
+
f"background: linear-gradient(135deg, {primary_color} 0%, {secondary_color} 100%);"
|
|
152
|
+
f" border: none; box-shadow: 0 10px 24px rgba({int(primary_color[1:3], 16)}, {int(primary_color[3:5], 16)}, {int(primary_color[5:7], 16)}, 0.35);"
|
|
153
|
+
if primary_color.startswith("#") else
|
|
154
|
+
"background: linear-gradient(135deg, #0d6efd 0%, #6610f2 100%);"
|
|
155
|
+
" border: none; box-shadow: 0 10px 24px rgba(13, 110, 253, 0.35);"
|
|
156
|
+
)
|
|
157
|
+
else:
|
|
158
|
+
# 使用单色按钮,稍微变亮作为按钮色
|
|
159
|
+
style_default = (
|
|
160
|
+
f"background: {secondary_color or primary_color};"
|
|
161
|
+
f" border: 1px solid {primary_color}; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);"
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
base = {
|
|
165
|
+
"text": "立即咨询",
|
|
166
|
+
"short_text": "咨询",
|
|
167
|
+
"href": (normalized_items[0]["href"] if normalized_items else "#home"),
|
|
168
|
+
"icon": "fas fa-arrow-right",
|
|
169
|
+
"style": style_default,
|
|
170
|
+
"size_class": "btn-sm",
|
|
171
|
+
"extra_class": "",
|
|
172
|
+
"visible": True,
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
booking_item = _find_item(("预约", "预订", "booking", "reserve"))
|
|
176
|
+
contact_item = _find_item(("联系", "contact", "call", "客服"))
|
|
177
|
+
menu_item = _find_item(("menu", "菜单", "products", "服务"))
|
|
178
|
+
about_item = _find_item(("about", "关于", "故事", "团队"))
|
|
179
|
+
|
|
180
|
+
if booking_item:
|
|
181
|
+
base.update({
|
|
182
|
+
"text": "立即预约",
|
|
183
|
+
"short_text": "预约",
|
|
184
|
+
"href": booking_item.get("href", base["href"]),
|
|
185
|
+
"icon": "fas fa-calendar-check",
|
|
186
|
+
})
|
|
187
|
+
elif contact_item:
|
|
188
|
+
base.update({
|
|
189
|
+
"text": "联系我们",
|
|
190
|
+
"short_text": "联系",
|
|
191
|
+
"href": contact_item.get("href", base["href"]),
|
|
192
|
+
"icon": "fas fa-headset",
|
|
193
|
+
})
|
|
194
|
+
elif menu_item:
|
|
195
|
+
base.update({
|
|
196
|
+
"text": "查看菜单",
|
|
197
|
+
"short_text": "菜单",
|
|
198
|
+
"href": menu_item.get("href", base["href"]),
|
|
199
|
+
"icon": "fas fa-book-open",
|
|
200
|
+
})
|
|
201
|
+
elif about_item:
|
|
202
|
+
base.update({
|
|
203
|
+
"text": "认识我们",
|
|
204
|
+
"short_text": "了解",
|
|
205
|
+
"href": about_item.get("href", base["href"]),
|
|
206
|
+
"icon": "fas fa-user-friends",
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
if isinstance(raw_cta, dict):
|
|
210
|
+
user_defined = {k: v for k, v in raw_cta.items() if v is not None}
|
|
211
|
+
base.update(user_defined)
|
|
212
|
+
|
|
213
|
+
return base
|
|
214
|
+
|
|
215
|
+
def _render_cta(cfg: dict) -> str:
|
|
216
|
+
if not cfg.get("visible", True):
|
|
217
|
+
return ""
|
|
218
|
+
text = cfg.get("text") or "立即咨询"
|
|
219
|
+
short_text = cfg.get("short_text") or text
|
|
220
|
+
href = cfg.get("href") or "#"
|
|
221
|
+
icon_class = cfg.get("icon") or ""
|
|
222
|
+
style = cfg.get("style") or (
|
|
223
|
+
"background: linear-gradient(135deg, #0d6efd 0%, #6610f2 100%);"
|
|
224
|
+
" border: none; box-shadow: 0 10px 24px rgba(13, 110, 253, 0.35);"
|
|
225
|
+
)
|
|
226
|
+
size_class = cfg.get("size_class") or "btn-sm"
|
|
227
|
+
extra_class = cfg.get("extra_class") or ""
|
|
228
|
+
classes = f"btn btn-gradient {size_class} rounded-pill px-3 text-white fw-bold"
|
|
229
|
+
if extra_class:
|
|
230
|
+
classes += f" {extra_class.strip()}"
|
|
231
|
+
icon_html = f"<i class=\"{icon_class} me-1\"></i>" if icon_class else ""
|
|
232
|
+
return (
|
|
233
|
+
f" <a class=\"{classes}\" href=\"{href}\" style=\"{style}\">\n"
|
|
234
|
+
f" {icon_html}<span class=\"d-none d-md-inline\">{text}</span>"
|
|
235
|
+
f"<span class=\"d-md-none\">{short_text}</span>\n"
|
|
236
|
+
f" </a>\n"
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
cta_block = _render_cta(_derive_cta(cta))
|
|
240
|
+
|
|
241
|
+
navbar_core = (
|
|
242
|
+
f"<nav class=\"navbar navbar-expand-lg navbar-dark {custom_class}\" style=\"{navbar_bg_style}\">\n"
|
|
243
|
+
f" <div class=\"container-xxl\">\n"
|
|
244
|
+
f" <a class=\"navbar-brand d-flex align-items-center\" href=\"{brand_href}\" style=\"color: {text_color};\">\n"
|
|
245
|
+
f" <span class=\"me-2\">☕</span>{brand_name}\n"
|
|
246
|
+
f" </a>\n"
|
|
247
|
+
f" <button class=\"navbar-toggler border-0\" type=\"button\" data-bs-toggle=\"collapse\" data-bs-target=\"#navbarNav\" aria-controls=\"navbarNav\" aria-expanded=\"false\" aria-label=\"切换导航\">\n"
|
|
248
|
+
f" <span class=\"navbar-toggler-icon\"></span>\n"
|
|
249
|
+
f" </button>\n"
|
|
250
|
+
f" <div class=\"collapse navbar-collapse\" id=\"navbarNav\">\n"
|
|
251
|
+
f" <ul class=\"navbar-nav ms-auto\">\n"
|
|
252
|
+
f"{nav_items_html} </ul>\n"
|
|
253
|
+
f" <div class=\"d-flex align-items-center ms-3 gap-2\">\n"
|
|
254
|
+
f" <button class=\"theme-toggle-btn btn btn-outline-light btn-sm rounded-pill px-3\" type=\"button\" aria-label=\"切换主题\" data-action=\"toggle-theme\" style=\"border: 1px solid rgba(255,255,255,0.3); backdrop-filter: blur(10px); color: {text_color};\">\n"
|
|
255
|
+
f" <i class=\"fas fa-moon me-1\"></i><span class=\"d-none d-md-inline\">夜间</span>\n"
|
|
256
|
+
f" </button>\n"
|
|
257
|
+
f"{cta_block}"
|
|
258
|
+
f" </div>\n"
|
|
259
|
+
f" </div>\n"
|
|
260
|
+
f" </div>\n"
|
|
261
|
+
f"</nav>"
|
|
262
|
+
)
|
|
263
|
+
# 用标记包裹,便于重复调用时检测并替换,保证幂等
|
|
264
|
+
navbar_html = "<!-- AI-Navbar: start -->\n" + navbar_core + "\n<!-- AI-Navbar: end -->"
|
|
265
|
+
|
|
266
|
+
try:
|
|
267
|
+
# 如果是HTML文件,直接插入导航栏
|
|
268
|
+
if file_path.endswith('.html'):
|
|
269
|
+
with open(file_path, "r", encoding="utf-8") as f:
|
|
270
|
+
content = f.read()
|
|
271
|
+
|
|
272
|
+
# 移除遗留的 script.js 引用,避免指向不存在的脚本
|
|
273
|
+
content = re.sub(
|
|
274
|
+
r"\s*<script[^>]*assets/js/script\.js[^>]*>\s*</script>",
|
|
275
|
+
"",
|
|
276
|
+
content,
|
|
277
|
+
flags=re.I,
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
# 尝试清理页面顶部的“简易锚点列表导航”(ul>li>a href="#..."),避免出现两个导航
|
|
281
|
+
try:
|
|
282
|
+
low = content.lower()
|
|
283
|
+
body_idx = low.find('<body')
|
|
284
|
+
insert_start = 0
|
|
285
|
+
if body_idx != -1:
|
|
286
|
+
gt = low.find('>', body_idx)
|
|
287
|
+
insert_start = gt + 1 if gt != -1 else 0
|
|
288
|
+
# 仅在文首 4000 字符内查找,降低误删风险
|
|
289
|
+
window_end = min(len(content), insert_start + 4000)
|
|
290
|
+
head_slice = content[insert_start:window_end]
|
|
291
|
+
|
|
292
|
+
ul_pattern = re.compile(r"<ul[^>]*>(?P<items>(?:\s*<li[^>]*>\s*<a[^>]*href=\s*['\"]#.+?['\"][^>]*>.*?</a>\s*</li>){3,}\s*)</ul>", re.I | re.S)
|
|
293
|
+
candidates = list(ul_pattern.finditer(head_slice))
|
|
294
|
+
for m in candidates:
|
|
295
|
+
items = m.group('items')
|
|
296
|
+
li_count = len(re.findall(r"<li\b", items, re.I))
|
|
297
|
+
anchor_count = len(re.findall(r"href=\s*['\"]#", items, re.I))
|
|
298
|
+
label_texts = [t.strip().lower() for t in re.findall(r">\s*([^<]{1,30})\s*</a>", items)]
|
|
299
|
+
common_labels = {"home", "products", "reviews", "contact", "about", "services", "案例", "产品", "联系", "关于", "首页", "服务"}
|
|
300
|
+
has_common = any(txt in common_labels for txt in label_texts)
|
|
301
|
+
# 需满足:至少3项内部锚点,且匹配常见导航标签或 ul 上 class 提示
|
|
302
|
+
ul_tag_open = head_slice[max(0, m.start()-200):m.end()]
|
|
303
|
+
class_hint = bool(re.search(r"class=\"[^\"]*(nav|menu|links)\b", ul_tag_open, re.I))
|
|
304
|
+
if li_count >= 3 and anchor_count >= 3 and (has_common or class_hint):
|
|
305
|
+
# 删除该 UL,避免重复导航
|
|
306
|
+
abs_start = insert_start + m.start()
|
|
307
|
+
abs_end = insert_start + m.end()
|
|
308
|
+
content = content[:abs_start] + content[abs_end:]
|
|
309
|
+
break
|
|
310
|
+
except Exception:
|
|
311
|
+
# 清理失败不影响主流程
|
|
312
|
+
pass
|
|
313
|
+
|
|
314
|
+
# 1) 已存在我们插入的导航,直接替换
|
|
315
|
+
if "<!-- AI-Navbar: start -->" in content and "<!-- AI-Navbar: end -->" in content:
|
|
316
|
+
content = re.sub(r"<!-- AI-Navbar: start -->.*?<!-- AI-Navbar: end -->", navbar_html, content, flags=re.S)
|
|
317
|
+
else:
|
|
318
|
+
# 2) 若存在任何带 .navbar 的 <nav>,替换第一个为我们的导航
|
|
319
|
+
pattern = re.compile(r"<nav[^>]*class=\"[^\"]*navbar[^\"]*\"[^>]*>.*?</nav>", re.S | re.I)
|
|
320
|
+
if pattern.search(content):
|
|
321
|
+
content = pattern.sub(navbar_html, content, count=1)
|
|
322
|
+
else:
|
|
323
|
+
# 3) 否则在 <body...> 标签闭合后插入
|
|
324
|
+
lower = content.lower()
|
|
325
|
+
body_idx = lower.find('<body')
|
|
326
|
+
if body_idx != -1:
|
|
327
|
+
gt_idx = lower.find('>', body_idx)
|
|
328
|
+
if gt_idx != -1:
|
|
329
|
+
content = content[:gt_idx+1] + "\n " + navbar_html + "\n" + content[gt_idx+1:]
|
|
330
|
+
else:
|
|
331
|
+
content = content.replace("<body>", f"<body>\n {navbar_html}\n")
|
|
332
|
+
else:
|
|
333
|
+
# 没找到 body,兜底:前置插入
|
|
334
|
+
content = navbar_html + "\n" + content
|
|
335
|
+
|
|
336
|
+
# 确保导航锚点对应的区块存在
|
|
337
|
+
id_pattern = re.compile(r'id\s*=\s*["\']([^"\']+)["\']', re.I)
|
|
338
|
+
existing_ids = {m.group(1).strip().lower() for m in id_pattern.finditer(content)}
|
|
339
|
+
missing_sections: list[str] = []
|
|
340
|
+
for item in normalized_items:
|
|
341
|
+
href = (item.get("href") or "").strip()
|
|
342
|
+
if not href.startswith("#") or len(href) <= 1:
|
|
343
|
+
continue
|
|
344
|
+
anchor = href[1:].strip()
|
|
345
|
+
if not anchor:
|
|
346
|
+
continue
|
|
347
|
+
if anchor.lower() in existing_ids:
|
|
348
|
+
continue
|
|
349
|
+
heading = html.escape(item.get("name") or anchor)
|
|
350
|
+
placeholder = (
|
|
351
|
+
f"\n <section id=\"{anchor}\" class=\"section py-5 bg-light ai-nav-placeholder\">\n"
|
|
352
|
+
" <div class=\"container text-center\">\n"
|
|
353
|
+
f" <h2 class=\"h4 text-secondary mb-3\">{heading}</h2>\n"
|
|
354
|
+
" <p class=\"text-muted\">此区块由导航自动生成,请替换为真实内容。</p>\n"
|
|
355
|
+
" </div>\n"
|
|
356
|
+
" </section>\n"
|
|
357
|
+
)
|
|
358
|
+
missing_sections.append(placeholder)
|
|
359
|
+
existing_ids.add(anchor.lower())
|
|
360
|
+
|
|
361
|
+
if missing_sections:
|
|
362
|
+
insertion = "".join(missing_sections)
|
|
363
|
+
body_close = re.search(r"</body>", content, re.I)
|
|
364
|
+
if body_close:
|
|
365
|
+
idx = body_close.start()
|
|
366
|
+
content = content[:idx] + insertion + "\n" + content[idx:]
|
|
367
|
+
else:
|
|
368
|
+
content = content + insertion
|
|
369
|
+
|
|
370
|
+
with open(file_path, "w", encoding="utf-8") as f:
|
|
371
|
+
f.write(content)
|
|
372
|
+
|
|
373
|
+
return f"响应式导航栏已添加到: {file_path}"
|
|
374
|
+
else:
|
|
375
|
+
# 创建独立的导航栏组件文件
|
|
376
|
+
Path(file_path).parent.mkdir(parents=True, exist_ok=True)
|
|
377
|
+
with open(file_path, "w", encoding="utf-8") as f:
|
|
378
|
+
f.write(navbar_html)
|
|
379
|
+
return f"导航栏组件文件已创建: {file_path}"
|
|
380
|
+
|
|
381
|
+
except Exception as e:
|
|
382
|
+
raise RuntimeError(f"创建导航栏失败: {str(e)}")
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
__all__ = ["create_responsive_navbar"]
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""项目结构相关工具"""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def create_project_structure(project_name: str, project_path: str) -> str:
|
|
8
|
+
"""创建网页项目的基础文件夹结构"""
|
|
9
|
+
if project_name in project_path:
|
|
10
|
+
base_path = Path(project_path)
|
|
11
|
+
else:
|
|
12
|
+
base_path = Path(project_path) / project_name
|
|
13
|
+
|
|
14
|
+
dirs = [
|
|
15
|
+
"css",
|
|
16
|
+
"js",
|
|
17
|
+
"images",
|
|
18
|
+
"assets",
|
|
19
|
+
"assets/css",
|
|
20
|
+
"assets/js",
|
|
21
|
+
"assets/images",
|
|
22
|
+
"components",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
try:
|
|
26
|
+
base_path.mkdir(parents=True, exist_ok=True)
|
|
27
|
+
for dir_name in dirs:
|
|
28
|
+
(base_path / dir_name).mkdir(parents=True, exist_ok=True)
|
|
29
|
+
return f"项目结构创建成功: {base_path}"
|
|
30
|
+
except Exception as exc: # noqa: BLE001
|
|
31
|
+
raise RuntimeError(f"创建项目结构失败: {str(exc)}")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
__all__ = ["create_project_structure"]
|