unifi-network-maps 1.4.4__py3-none-any.whl → 1.4.5__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 (33) hide show
  1. unifi_network_maps/__init__.py +1 -1
  2. unifi_network_maps/cli/__init__.py +2 -38
  3. unifi_network_maps/cli/args.py +166 -0
  4. unifi_network_maps/cli/main.py +18 -752
  5. unifi_network_maps/cli/render.py +255 -0
  6. unifi_network_maps/cli/runtime.py +157 -0
  7. unifi_network_maps/io/mkdocs_assets.py +21 -0
  8. unifi_network_maps/io/mock_generate.py +2 -294
  9. unifi_network_maps/model/mock.py +307 -0
  10. unifi_network_maps/render/device_ports_md.py +44 -27
  11. unifi_network_maps/render/legend.py +30 -0
  12. unifi_network_maps/render/lldp_md.py +81 -60
  13. unifi_network_maps/render/markdown_tables.py +21 -0
  14. unifi_network_maps/render/mermaid.py +72 -85
  15. unifi_network_maps/render/mkdocs.py +167 -0
  16. unifi_network_maps/render/templates/device_port_block.md.j2 +5 -0
  17. unifi_network_maps/render/templates/legend_compact.html.j2 +14 -0
  18. unifi_network_maps/render/templates/lldp_device_section.md.j2 +15 -0
  19. unifi_network_maps/render/templates/markdown_section.md.j2 +3 -0
  20. unifi_network_maps/render/templates/mermaid_legend.mmd.j2 +30 -0
  21. unifi_network_maps/render/templates/mkdocs_document.md.j2 +23 -0
  22. unifi_network_maps/render/templates/mkdocs_dual_theme_style.html.j2 +8 -0
  23. unifi_network_maps/render/templates/mkdocs_html_block.html.j2 +2 -0
  24. unifi_network_maps/render/templates/mkdocs_legend.css.j2 +29 -0
  25. unifi_network_maps/render/templates/mkdocs_legend.js.j2 +18 -0
  26. unifi_network_maps/render/templates/mkdocs_mermaid_block.md.j2 +4 -0
  27. unifi_network_maps/render/templating.py +19 -0
  28. {unifi_network_maps-1.4.4.dist-info → unifi_network_maps-1.4.5.dist-info}/METADATA +2 -1
  29. {unifi_network_maps-1.4.4.dist-info → unifi_network_maps-1.4.5.dist-info}/RECORD +33 -13
  30. {unifi_network_maps-1.4.4.dist-info → unifi_network_maps-1.4.5.dist-info}/WHEEL +0 -0
  31. {unifi_network_maps-1.4.4.dist-info → unifi_network_maps-1.4.5.dist-info}/entry_points.txt +0 -0
  32. {unifi_network_maps-1.4.4.dist-info → unifi_network_maps-1.4.5.dist-info}/licenses/LICENSE +0 -0
  33. {unifi_network_maps-1.4.4.dist-info → unifi_network_maps-1.4.5.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,30 @@
1
+ """Legend rendering helpers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from .mermaid import render_legend, render_legend_compact
6
+ from .mermaid_theme import MermaidTheme
7
+
8
+
9
+ def resolve_legend_style(*, format_name: str, legend_style: str) -> str:
10
+ if legend_style == "auto":
11
+ return "compact" if format_name == "mkdocs" else "diagram"
12
+ return legend_style
13
+
14
+
15
+ def render_legend_only(
16
+ *,
17
+ legend_style: str,
18
+ legend_scale: float,
19
+ markdown: bool,
20
+ theme: MermaidTheme,
21
+ ) -> str:
22
+ if legend_style == "compact":
23
+ content = "# Legend\n\n" + render_legend_compact(theme=theme)
24
+ else:
25
+ content = render_legend(theme=theme, legend_scale=legend_scale)
26
+ if markdown:
27
+ content = f"""```mermaid
28
+ {content}```
29
+ """
30
+ return content
@@ -7,6 +7,8 @@ from collections.abc import Iterable
7
7
  from ..model.lldp import LLDPEntry, local_port_label
8
8
  from ..model.topology import Device, build_client_port_map, build_device_index, build_port_map
9
9
  from .device_ports_md import render_device_port_details
10
+ from .markdown_tables import markdown_table_lines
11
+ from .templating import render_template
10
12
 
11
13
 
12
14
  def _normalize_mac(value: str) -> str:
@@ -78,10 +80,6 @@ def _lldp_sort_key(entry: LLDPEntry) -> tuple[int, str, str]:
78
80
  return (int(port_number or 0), port_label, entry.port_id)
79
81
 
80
82
 
81
- def _device_header_lines(device: Device) -> list[str]:
82
- return [f"## {device.name}"]
83
-
84
-
85
83
  def _port_summary(device: Device) -> str:
86
84
  ports = [port for port in device.port_table if port.port_idx is not None]
87
85
  if not ports:
@@ -140,23 +138,20 @@ def _details_table_lines(
140
138
  ) -> list[str]:
141
139
  wired_count, client_sample = _client_summary(device, client_rows)
142
140
  client_label = f"Clients ({client_mode})"
143
- lines = [
144
- "### Details",
145
- "",
146
- "| Field | Value |",
147
- "| --- | --- |",
148
- f"| Model | {_escape_cell(device.model_name or device.type or '-')} |",
149
- f"| Type | {_escape_cell(device.type or '-')} |",
150
- f"| IP | {_escape_cell(device.ip or '-')} |",
151
- f"| MAC | {_escape_cell(device.mac or '-')} |",
152
- f"| Firmware | {_escape_cell(device.version or '-')} |",
153
- f"| Uplink | {_escape_cell(_uplink_summary(device))} |",
154
- f"| Ports | {_escape_cell(_port_summary(device))} |",
155
- f"| PoE | {_escape_cell(_poe_summary(device))} |",
156
- f"| {client_label} | {_escape_cell(wired_count)} |",
157
- f"| Client examples | {_escape_cell(client_sample)} |",
158
- "",
141
+ rows = [
142
+ ["Model", _escape_cell(device.model_name or device.type or "-")],
143
+ ["Type", _escape_cell(device.type or "-")],
144
+ ["IP", _escape_cell(device.ip or "-")],
145
+ ["MAC", _escape_cell(device.mac or "-")],
146
+ ["Firmware", _escape_cell(device.version or "-")],
147
+ ["Uplink", _escape_cell(_uplink_summary(device))],
148
+ ["Ports", _escape_cell(_port_summary(device))],
149
+ ["PoE", _escape_cell(_poe_summary(device))],
150
+ [client_label, _escape_cell(wired_count)],
151
+ ["Client examples", _escape_cell(client_sample)],
159
152
  ]
153
+ lines = ["### Details", ""]
154
+ lines.extend(markdown_table_lines(["Field", "Value"], rows))
160
155
  return lines
161
156
 
162
157
 
@@ -241,7 +236,6 @@ def _prepare_lldp_maps(
241
236
 
242
237
 
243
238
  def _render_device_lldp_section(
244
- lines: list[str],
245
239
  device: Device,
246
240
  *,
247
241
  device_index: dict[str, str],
@@ -251,40 +245,58 @@ def _render_device_lldp_section(
251
245
  include_ports: bool,
252
246
  show_clients: bool,
253
247
  client_mode: str,
254
- ) -> None:
255
- lines.extend(_device_header_lines(device))
256
- lines.append("")
257
- lines.extend(_details_table_lines(device, client_rows, client_mode))
248
+ ) -> str:
249
+ details = "\n".join(_details_table_lines(device, client_rows, client_mode)).rstrip()
250
+ ports_section = ""
258
251
  if include_ports:
259
- lines.append("### Ports")
260
- lines.append("")
261
- lines.append(
262
- render_device_port_details(device, port_map, client_ports=client_port_map).strip()
263
- )
252
+ ports_section = "\n".join(
253
+ [
254
+ "### Ports",
255
+ "",
256
+ render_device_port_details(device, port_map, client_ports=client_port_map).strip(),
257
+ ]
258
+ ).rstrip()
264
259
  if device.lldp_info:
265
- lines.append("")
266
- lines.append("| Local Port | Neighbor | Neighbor Port | Chassis ID | Port Description |")
267
- lines.append("| --- | --- | --- | --- | --- |")
268
- for row in _lldp_rows(device.lldp_info, device_index):
269
- lines.append("| " + " | ".join(_escape_cell(cell) for cell in row) + " |")
270
- lines.append("")
260
+ lldp_section = "\n".join(
261
+ markdown_table_lines(
262
+ ["Local Port", "Neighbor", "Neighbor Port", "Chassis ID", "Port Description"],
263
+ _lldp_rows(device.lldp_info, device_index),
264
+ escape=_escape_cell,
265
+ )
266
+ ).rstrip()
271
267
  else:
272
- lines.append("_No LLDP neighbors._")
273
- lines.append("")
268
+ lldp_section = "_No LLDP neighbors._"
269
+ clients_section = ""
274
270
  rows = client_rows.get(device.name)
275
271
  if rows and show_clients:
276
- lines.append("")
277
- lines.append("### Clients")
278
272
  if include_ports:
279
- lines.append("")
280
- lines.append("| Client | Port |")
281
- lines.append("| --- | --- |")
282
- for client_name, port_label in rows:
283
- lines.append(f"| {_escape_cell(client_name)} | {_escape_cell(port_label or '-')} |")
273
+ clients_section = "\n".join(
274
+ [
275
+ "### Clients",
276
+ "",
277
+ "\n".join(
278
+ markdown_table_lines(
279
+ ["Client", "Port"],
280
+ [
281
+ [_escape_cell(client_name), _escape_cell(port_label or "-")]
282
+ for client_name, port_label in rows
283
+ ],
284
+ )
285
+ ),
286
+ ]
287
+ ).rstrip()
284
288
  else:
285
- for client_name, _port_label in rows:
286
- lines.append(f"- {_escape_cell(client_name)}")
287
- lines.append("")
289
+ clients_section = "\n".join(
290
+ ["### Clients", *[f"- {_escape_cell(name)}" for name, _ in rows]]
291
+ ).rstrip()
292
+ return render_template(
293
+ "lldp_device_section.md.j2",
294
+ device_name=device.name,
295
+ details=details,
296
+ ports_section=ports_section,
297
+ lldp_section=lldp_section,
298
+ clients_section=clients_section,
299
+ ).rstrip()
288
300
 
289
301
 
290
302
  def render_lldp_md(
@@ -303,17 +315,26 @@ def render_lldp_md(
303
315
  show_clients=show_clients,
304
316
  client_mode=client_mode,
305
317
  )
306
- lines: list[str] = ["# LLDP Neighbors", ""]
318
+ sections: list[str] = []
307
319
  for device in sorted(devices, key=lambda item: item.name.lower()):
308
- _render_device_lldp_section(
309
- lines,
310
- device,
311
- device_index=device_index,
312
- port_map=port_map,
313
- client_port_map=client_port_map,
314
- client_rows=client_rows,
315
- include_ports=include_ports,
316
- show_clients=show_clients,
317
- client_mode=client_mode,
320
+ sections.append(
321
+ _render_device_lldp_section(
322
+ device,
323
+ device_index=device_index,
324
+ port_map=port_map,
325
+ client_port_map=client_port_map,
326
+ client_rows=client_rows,
327
+ include_ports=include_ports,
328
+ show_clients=show_clients,
329
+ client_mode=client_mode,
330
+ )
318
331
  )
319
- return "\n".join(lines).rstrip() + "\n"
332
+ body = "\n\n".join(section for section in sections if section).rstrip()
333
+ return (
334
+ render_template(
335
+ "markdown_section.md.j2",
336
+ title="LLDP Neighbors",
337
+ body=body,
338
+ ).rstrip()
339
+ + "\n"
340
+ )
@@ -0,0 +1,21 @@
1
+ """Markdown table helpers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Callable, Iterable
6
+
7
+
8
+ def markdown_table_lines(
9
+ headers: list[str],
10
+ rows: Iterable[Iterable[str]],
11
+ *,
12
+ escape: Callable[[str], str] | None = None,
13
+ ) -> list[str]:
14
+ esc = escape or (lambda value: value)
15
+ lines = [
16
+ "| " + " | ".join(headers) + " |",
17
+ "| " + " | ".join(["---"] * len(headers)) + " |",
18
+ ]
19
+ for row in rows:
20
+ lines.append("| " + " | ".join(esc(cell) for cell in row) + " |")
21
+ return lines
@@ -7,6 +7,7 @@ from collections.abc import Iterable
7
7
 
8
8
  from ..model.topology import Edge
9
9
  from .mermaid_theme import DEFAULT_THEME, MermaidTheme, class_defs
10
+ from .templating import render_template
10
11
 
11
12
 
12
13
  def _escape(label: str) -> str:
@@ -193,94 +194,80 @@ def render_legend(theme: MermaidTheme = DEFAULT_THEME, *, legend_scale: float =
193
194
  node_spacing = max(10, round(50 * scale))
194
195
  rank_spacing = max(10, round(50 * scale))
195
196
  node_padding = max(4, round(12 * scale))
196
- lines = [
197
- "%%{init: {"
198
- f'"flowchart": {{"nodeSpacing": {node_spacing}, "rankSpacing": {rank_spacing}}}, '
199
- f'"themeVariables": {{"fontSize": "{legend_font_size}px", "nodePadding": {node_padding}}}'
200
- "}}%%",
201
- "graph TB",
202
- ' subgraph legend["Legend"];',
203
- ' legend_gateway["Gateway"];',
204
- ' legend_switch["Switch"];',
205
- ' legend_ap["AP"];',
206
- ' legend_client["Client"];',
207
- ' legend_other["Other"];',
208
- ' legend_poe_a["PoE Link A"];',
209
- ' legend_poe_b["PoE Link B"];',
210
- ' legend_no_poe_a["Link A"];',
211
- ' legend_no_poe_b["Link B"];',
212
- " legend_poe_a ---|⚡| legend_poe_b;",
213
- " legend_no_poe_a --- legend_no_poe_b;",
214
- " linkStyle 0 arrowhead:none;",
215
- " linkStyle 1 arrowhead:none;",
216
- " end",
217
- " class legend_gateway node_gateway;",
218
- " class legend_switch node_switch;",
219
- " class legend_ap node_ap;",
220
- " class legend_client node_client;",
221
- " class legend_other node_other;",
222
- " class legend_poe_a node_legend;",
223
- " class legend_poe_b node_legend;",
224
- " class legend_no_poe_a node_legend;",
225
- " class legend_no_poe_b node_legend;",
226
- ]
227
- lines.extend(class_defs(theme))
228
- lines.append(f" classDef node_legend font-size:{legend_font_size}px;")
229
- lines.append(
230
- " linkStyle 0 "
231
- f"stroke:{theme.poe_link},stroke-width:{poe_link_width}px,"
232
- f"arrowhead:{theme.poe_link_arrow};"
233
- )
234
- lines.append(
235
- " linkStyle 1 "
236
- f"stroke:{theme.standard_link},stroke-width:{standard_link_width}px,"
237
- f"arrowhead:{theme.standard_link_arrow};"
197
+ return (
198
+ render_template(
199
+ "mermaid_legend.mmd.j2",
200
+ node_spacing=node_spacing,
201
+ rank_spacing=rank_spacing,
202
+ legend_font_size=legend_font_size,
203
+ node_padding=node_padding,
204
+ class_defs="\n".join(class_defs(theme)),
205
+ poe_link=theme.poe_link,
206
+ poe_link_width=poe_link_width,
207
+ poe_link_arrow=theme.poe_link_arrow,
208
+ standard_link=theme.standard_link,
209
+ standard_link_width=standard_link_width,
210
+ standard_link_arrow=theme.standard_link_arrow,
211
+ ).rstrip()
212
+ + "\n"
238
213
  )
239
- return "\n".join(lines) + "\n"
240
214
 
241
215
 
242
216
  def render_legend_compact(theme: MermaidTheme = DEFAULT_THEME) -> str:
243
- def swatch(fill: str, stroke: str, label: str) -> str:
244
- return (
245
- f'<span style="display:inline-block;width:12px;height:12px;'
246
- f"background:{fill};border:1px solid {stroke};border-radius:2px;"
247
- f'margin-right:6px;"></span>{label}'
248
- )
249
-
250
- def line_sample(
251
- color: str,
252
- width: int,
253
- *,
254
- dashed: bool = False,
255
- label: str = "",
256
- bolt: bool = False,
257
- ) -> str:
258
- dash = ' stroke-dasharray="5 4"' if dashed else ""
259
- bolt_suffix = " ⚡" if bolt else ""
260
- return (
261
- f'<span style="display:inline-flex;align-items:center;gap:6px;">'
262
- f'<svg width="42" height="10" viewBox="0 0 42 10" '
263
- f'xmlns="http://www.w3.org/2000/svg" aria-hidden="true">'
264
- f'<line x1="2" y1="5" x2="40" y2="5" stroke="{color}" '
265
- f'stroke-width="{max(1, width)}"{dash} />'
266
- f"</svg>{label}{bolt_suffix}</span>"
267
- )
268
-
269
217
  rows = [
270
- swatch(theme.node_gateway[0], theme.node_gateway[1], "Gateway"),
271
- swatch(theme.node_switch[0], theme.node_switch[1], "Switch"),
272
- swatch(theme.node_ap[0], theme.node_ap[1], "AP"),
273
- swatch(theme.node_client[0], theme.node_client[1], "Client"),
274
- swatch(theme.node_other[0], theme.node_other[1], "Other"),
275
- line_sample(theme.poe_link, theme.poe_link_width, label="PoE", bolt=True),
276
- line_sample(theme.standard_link, theme.standard_link_width, label="Link"),
277
- line_sample(theme.standard_link, theme.standard_link_width, dashed=True, label="Wireless"),
218
+ {
219
+ "kind": "swatch",
220
+ "fill": theme.node_gateway[0],
221
+ "stroke": theme.node_gateway[1],
222
+ "label": "Gateway",
223
+ },
224
+ {
225
+ "kind": "swatch",
226
+ "fill": theme.node_switch[0],
227
+ "stroke": theme.node_switch[1],
228
+ "label": "Switch",
229
+ },
230
+ {
231
+ "kind": "swatch",
232
+ "fill": theme.node_ap[0],
233
+ "stroke": theme.node_ap[1],
234
+ "label": "AP",
235
+ },
236
+ {
237
+ "kind": "swatch",
238
+ "fill": theme.node_client[0],
239
+ "stroke": theme.node_client[1],
240
+ "label": "Client",
241
+ },
242
+ {
243
+ "kind": "swatch",
244
+ "fill": theme.node_other[0],
245
+ "stroke": theme.node_other[1],
246
+ "label": "Other",
247
+ },
248
+ {
249
+ "kind": "line",
250
+ "color": theme.poe_link,
251
+ "width": max(1, theme.poe_link_width),
252
+ "dashed": False,
253
+ "label": "PoE",
254
+ "bolt": True,
255
+ },
256
+ {
257
+ "kind": "line",
258
+ "color": theme.standard_link,
259
+ "width": max(1, theme.standard_link_width),
260
+ "dashed": False,
261
+ "label": "Link",
262
+ "bolt": False,
263
+ },
264
+ {
265
+ "kind": "line",
266
+ "color": theme.standard_link,
267
+ "width": max(1, theme.standard_link_width),
268
+ "dashed": True,
269
+ "label": "Wireless",
270
+ "bolt": False,
271
+ },
278
272
  ]
279
- lines = [
280
- '<table class="unifi-legend-table">',
281
- "<tbody>",
282
- ]
283
- lines.extend(f" <tr><td>{style}</td></tr>" for style in rows)
284
- lines.append("</tbody>")
285
- lines.append("</table>")
286
- return "\n".join(lines) + "\n"
273
+ return render_template("legend_compact.html.j2", rows=rows)
@@ -0,0 +1,167 @@
1
+ """MkDocs-specific rendering helpers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ from dataclasses import dataclass
7
+ from datetime import datetime
8
+ from zoneinfo import ZoneInfo
9
+
10
+ from ..model.topology import ClientPortMap, Device, PortMap, build_node_type_map
11
+ from .device_ports_md import render_device_port_overview
12
+ from .mermaid import render_legend, render_legend_compact, render_mermaid
13
+ from .mermaid_theme import MermaidTheme
14
+ from .templating import render_template
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ @dataclass(frozen=True)
20
+ class MkdocsRenderOptions:
21
+ direction: str
22
+ legend_style: str
23
+ legend_scale: float
24
+ timestamp_zone: str
25
+ client_scope: str
26
+ dual_theme: bool
27
+
28
+
29
+ def render_mkdocs(
30
+ edges: list,
31
+ devices: list[Device],
32
+ *,
33
+ mermaid_theme: MermaidTheme,
34
+ port_map: PortMap,
35
+ client_ports: ClientPortMap | None,
36
+ options: MkdocsRenderOptions,
37
+ dark_mermaid_theme: MermaidTheme | None = None,
38
+ ) -> str:
39
+ clients = None
40
+ node_types = build_node_type_map(devices, clients, client_mode=options.client_scope)
41
+ content = render_mermaid(
42
+ edges,
43
+ direction=options.direction,
44
+ node_types=node_types,
45
+ theme=mermaid_theme,
46
+ )
47
+ dual_theme = options.dual_theme and dark_mermaid_theme is not None
48
+ legend_title = "Legend" if options.legend_style != "compact" else ""
49
+ if dual_theme and dark_mermaid_theme is not None:
50
+ dark_content = render_mermaid(
51
+ edges,
52
+ direction=options.direction,
53
+ node_types=node_types,
54
+ theme=dark_mermaid_theme,
55
+ )
56
+ map_block = _mkdocs_dual_mermaid_block(content, dark_content, base_class="unifi-mermaid")
57
+ legend_block = _mkdocs_dual_legend_block(
58
+ options.legend_style,
59
+ mermaid_theme=mermaid_theme,
60
+ dark_mermaid_theme=dark_mermaid_theme,
61
+ legend_scale=options.legend_scale,
62
+ )
63
+ dual_style = _mkdocs_dual_theme_style()
64
+ else:
65
+ map_block = _mkdocs_mermaid_block(content, class_name="unifi-mermaid")
66
+ legend_block = _mkdocs_single_legend_block(
67
+ options.legend_style,
68
+ mermaid_theme=mermaid_theme,
69
+ legend_scale=options.legend_scale,
70
+ )
71
+ dual_style = ""
72
+ return render_template(
73
+ "mkdocs_document.md.j2",
74
+ title="UniFi network",
75
+ timestamp_line=_timestamp_line(options.timestamp_zone),
76
+ dual_style=dual_style,
77
+ map_block=map_block,
78
+ legend_title=legend_title,
79
+ legend_block=legend_block,
80
+ device_overview=render_device_port_overview(
81
+ devices, port_map, client_ports=client_ports
82
+ ).rstrip()
83
+ + "\n",
84
+ )
85
+
86
+
87
+ def _timestamp_line(timestamp_zone: str) -> str:
88
+ if timestamp_zone.strip().lower() in {"off", "none", "false"}:
89
+ return ""
90
+ try:
91
+ zone = ZoneInfo(timestamp_zone)
92
+ except Exception as exc: # noqa: BLE001
93
+ logger.warning("Invalid mkdocs timestamp zone '%s': %s", timestamp_zone, exc)
94
+ return ""
95
+ generated_at = datetime.now(zone).strftime("%Y-%m-%d %H:%M:%S %Z")
96
+ return f"Generated: {generated_at}"
97
+
98
+
99
+ def _mkdocs_mermaid_block(content: str, *, class_name: str) -> str:
100
+ return render_template(
101
+ "mkdocs_mermaid_block.md.j2",
102
+ class_name=class_name,
103
+ content=content,
104
+ )
105
+
106
+
107
+ def _mkdocs_dual_mermaid_block(
108
+ light_content: str,
109
+ dark_content: str,
110
+ *,
111
+ base_class: str,
112
+ ) -> str:
113
+ light = _mkdocs_mermaid_block(light_content, class_name=f"{base_class} {base_class}--light")
114
+ dark = _mkdocs_mermaid_block(dark_content, class_name=f"{base_class} {base_class}--dark")
115
+ return f"{light}\n{dark}"
116
+
117
+
118
+ def _mkdocs_single_legend_block(
119
+ legend_style: str,
120
+ *,
121
+ mermaid_theme: MermaidTheme,
122
+ legend_scale: float,
123
+ ) -> str:
124
+ if legend_style == "compact":
125
+ return render_template(
126
+ "mkdocs_html_block.html.j2",
127
+ class_name="unifi-legend",
128
+ data_unifi_legend=True,
129
+ content=render_legend_compact(theme=mermaid_theme),
130
+ )
131
+ return "```mermaid\n" + render_legend(theme=mermaid_theme, legend_scale=legend_scale) + "```"
132
+
133
+
134
+ def _mkdocs_dual_legend_block(
135
+ legend_style: str,
136
+ *,
137
+ mermaid_theme: MermaidTheme,
138
+ dark_mermaid_theme: MermaidTheme,
139
+ legend_scale: float,
140
+ ) -> str:
141
+ if legend_style == "compact":
142
+ light = render_template(
143
+ "mkdocs_html_block.html.j2",
144
+ class_name="unifi-legend unifi-legend--light",
145
+ data_unifi_legend=True,
146
+ content=render_legend_compact(theme=mermaid_theme),
147
+ )
148
+ dark = render_template(
149
+ "mkdocs_html_block.html.j2",
150
+ class_name="unifi-legend unifi-legend--dark",
151
+ data_unifi_legend=True,
152
+ content=render_legend_compact(theme=dark_mermaid_theme),
153
+ )
154
+ return f"{light}\n{dark}"
155
+ light = _mkdocs_mermaid_block(
156
+ render_legend(theme=mermaid_theme, legend_scale=legend_scale),
157
+ class_name="unifi-legend unifi-legend--light",
158
+ )
159
+ dark = _mkdocs_mermaid_block(
160
+ render_legend(theme=dark_mermaid_theme, legend_scale=legend_scale),
161
+ class_name="unifi-legend unifi-legend--dark",
162
+ )
163
+ return f"{light}\n{dark}"
164
+
165
+
166
+ def _mkdocs_dual_theme_style() -> str:
167
+ return render_template("mkdocs_dual_theme_style.html.j2") + "\n"
@@ -0,0 +1,5 @@
1
+ ### {{ device_name }}
2
+
3
+ {{ details }}
4
+
5
+ {{ ports }}
@@ -0,0 +1,14 @@
1
+ <table class="unifi-legend-table">
2
+ <tbody>
3
+ {% for row in rows %}
4
+ <tr><td>
5
+ {% if row.kind == "swatch" %}
6
+ <span style="display:inline-block;width:12px;height:12px;background:{{ row.fill }};border:1px solid {{ row.stroke }};border-radius:2px;margin-right:6px;"></span>{{ row.label }}
7
+ {% else %}
8
+ <span style="display:inline-flex;align-items:center;gap:6px;">
9
+ <svg width="42" height="10" viewBox="0 0 42 10" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"><line x1="2" y1="5" x2="40" y2="5" stroke="{{ row.color }}" stroke-width="{{ row.width }}"{% if row.dashed %} stroke-dasharray="5 4"{% endif %} /></svg>{{ row.label }}{%- if row.bolt %} ⚡{%- endif %}</span>
10
+ {% endif %}
11
+ </td></tr>
12
+ {% endfor %}
13
+ </tbody>
14
+ </table>
@@ -0,0 +1,15 @@
1
+ ## {{ device_name }}
2
+
3
+ {{ details }}
4
+ {% if ports_section %}
5
+
6
+ {{ ports_section }}
7
+ {% endif %}
8
+ {% if lldp_section %}
9
+
10
+ {{ lldp_section }}
11
+ {% endif %}
12
+ {% if clients_section %}
13
+
14
+ {{ clients_section }}
15
+ {% endif %}
@@ -0,0 +1,3 @@
1
+ ## {{ title }}
2
+
3
+ {{ body }}
@@ -0,0 +1,30 @@
1
+ %%{init: {"flowchart": {"nodeSpacing": {{ node_spacing }}, "rankSpacing": {{ rank_spacing }}}, "themeVariables": {"fontSize": "{{ legend_font_size }}px", "nodePadding": {{ node_padding }}}}}%%
2
+ graph TB
3
+ subgraph legend["Legend"];
4
+ legend_gateway["Gateway"];
5
+ legend_switch["Switch"];
6
+ legend_ap["AP"];
7
+ legend_client["Client"];
8
+ legend_other["Other"];
9
+ legend_poe_a["PoE Link A"];
10
+ legend_poe_b["PoE Link B"];
11
+ legend_no_poe_a["Link A"];
12
+ legend_no_poe_b["Link B"];
13
+ legend_poe_a ---|⚡| legend_poe_b;
14
+ legend_no_poe_a --- legend_no_poe_b;
15
+ linkStyle 0 arrowhead:none;
16
+ linkStyle 1 arrowhead:none;
17
+ end
18
+ class legend_gateway node_gateway;
19
+ class legend_switch node_switch;
20
+ class legend_ap node_ap;
21
+ class legend_client node_client;
22
+ class legend_other node_other;
23
+ class legend_poe_a node_legend;
24
+ class legend_poe_b node_legend;
25
+ class legend_no_poe_a node_legend;
26
+ class legend_no_poe_b node_legend;
27
+ {{ class_defs }}
28
+ classDef node_legend font-size:{{ legend_font_size }}px;
29
+ linkStyle 0 stroke:{{ poe_link }},stroke-width:{{ poe_link_width }}px,arrowhead:{{ poe_link_arrow }};
30
+ linkStyle 1 stroke:{{ standard_link }},stroke-width:{{ standard_link_width }}px,arrowhead:{{ standard_link_arrow }};