Skip to content

Commit cf4318b

Browse files
authored
Fix Mermaid dark-mode contrast and add auto-remediation for diagram colors (#2921)
Remove the blanket dark-mode text-color override in custom.css that was forcing near-black text onto unstyled Mermaid diagrams, hiding labels that Mermaid's own dark theme already renders with correct contrast (e.g. the SNMP traps receiver pipeline diagram). Diagrams with explicit classDef/style colors are unaffected since they already declare their own text color. Add a contrast check to ingest.py, reusing its existing WCAG helpers from the logo-contrast analysis, that scans classDef/style fill+color pairs across all docs and auto-rewrites any pair below 4.5:1 (WCAG AA) to a safe default colorway.
1 parent 61a151f commit cf4318b

2 files changed

Lines changed: 124 additions & 26 deletions

File tree

ingest/ingest.py

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,18 @@
129129
"unknown": 0,
130130
}
131131

132+
# WCAG AA normal-text threshold. Mermaid classDef/style fill+color pairs below
133+
# this are rewritten to MERMAID_DEFAULT_* since a fixed hex fill can't adapt to
134+
# light/dark colorMode on its own.
135+
MERMAID_CONTRAST_THRESHOLD = 4.5
136+
MERMAID_DEFAULT_FILL = "#f0f0f0"
137+
MERMAID_DEFAULT_STROKE = "#666666"
138+
MERMAID_DEFAULT_TEXT = "#1a1a1a"
139+
MERMAID_CONTRAST_SUMMARY = {
140+
"scanned": 0,
141+
"fixed": 0,
142+
}
143+
132144
MAP_COLUMNS = [
133145
"custom_edit_url",
134146
"sidebar_label",
@@ -1889,6 +1901,111 @@ def _analyze_remote_logo(url):
18891901
return result
18901902

18911903

1904+
_MERMAID_FENCE_RE = re.compile(r"```mermaid[ \t]*\n(.*?)```", re.DOTALL)
1905+
_MERMAID_STYLE_LINE_RE = re.compile(r"^(\s*)(classDef|style)\s+(\S+)\s+(.+?)\s*$")
1906+
1907+
1908+
def _parse_mermaid_style_props(props_str):
1909+
props = {}
1910+
for part in props_str.split(","):
1911+
if ":" not in part:
1912+
continue
1913+
key, _, value = part.partition(":")
1914+
props[key.strip().lower()] = value.strip()
1915+
return props
1916+
1917+
1918+
def _rebuild_mermaid_style_props(props_str, updates):
1919+
rebuilt = []
1920+
for part in props_str.split(","):
1921+
if ":" not in part:
1922+
rebuilt.append(part)
1923+
continue
1924+
key, _, value = part.partition(":")
1925+
key_lower = key.strip().lower()
1926+
if key_lower in updates:
1927+
rebuilt.append(f"{key.strip()}: {updates[key_lower]}")
1928+
else:
1929+
rebuilt.append(f"{key.strip()}:{value}")
1930+
return ",".join(rebuilt)
1931+
1932+
1933+
def _fix_mermaid_diagram_contrast_in_text(text):
1934+
changes = []
1935+
1936+
def _process_block(match):
1937+
block = match.group(1)
1938+
new_lines = []
1939+
for line in block.split("\n"):
1940+
style_match = _MERMAID_STYLE_LINE_RE.match(line)
1941+
if not style_match:
1942+
new_lines.append(line)
1943+
continue
1944+
1945+
indent, keyword, target, props_str = style_match.groups()
1946+
props = _parse_mermaid_style_props(props_str)
1947+
fill = props.get("fill")
1948+
color = props.get("color")
1949+
if not fill or not color:
1950+
new_lines.append(line)
1951+
continue
1952+
1953+
fill_rgb = _parse_css_color(fill)
1954+
color_rgb = _parse_css_color(color)
1955+
if not fill_rgb or not color_rgb:
1956+
new_lines.append(line)
1957+
continue
1958+
1959+
MERMAID_CONTRAST_SUMMARY["scanned"] += 1
1960+
ratio = _contrast_ratio(
1961+
_relative_luminance(fill_rgb[:3]),
1962+
_relative_luminance(color_rgb[:3]),
1963+
)
1964+
if ratio >= MERMAID_CONTRAST_THRESHOLD:
1965+
new_lines.append(line)
1966+
continue
1967+
1968+
updates = {"fill": MERMAID_DEFAULT_FILL, "color": MERMAID_DEFAULT_TEXT}
1969+
if "stroke" in props:
1970+
updates["stroke"] = MERMAID_DEFAULT_STROKE
1971+
new_lines.append(
1972+
f"{indent}{keyword} {target} "
1973+
f"{_rebuild_mermaid_style_props(props_str, updates)}"
1974+
)
1975+
changes.append((keyword, target, fill, color, ratio))
1976+
1977+
return "```mermaid\n" + "\n".join(new_lines) + "```"
1978+
1979+
new_text = _MERMAID_FENCE_RE.sub(_process_block, text)
1980+
return new_text, changes
1981+
1982+
1983+
def fix_mermaid_diagram_contrast(docs_prefix):
1984+
for path in glob.glob(f"{docs_prefix}/**/*.mdx", recursive=True):
1985+
try:
1986+
with open(path, "r", encoding="utf-8") as fh:
1987+
original = fh.read()
1988+
except OSError:
1989+
continue
1990+
1991+
if "```mermaid" not in original:
1992+
continue
1993+
1994+
new_text, changes = _fix_mermaid_diagram_contrast_in_text(original)
1995+
if not changes:
1996+
continue
1997+
1998+
with open(path, "w", encoding="utf-8") as fh:
1999+
fh.write(new_text)
2000+
2001+
MERMAID_CONTRAST_SUMMARY["fixed"] += len(changes)
2002+
for keyword, target, old_fill, old_color, ratio in changes:
2003+
print(
2004+
f" {path}: {keyword} {target} contrast {ratio:.2f}:1 "
2005+
f"(fill {old_fill}, color {old_color}) -> default colorway"
2006+
)
2007+
2008+
18922009
def _set_html_attr(tag, attr_name, attr_value):
18932010
attr_regex = re.compile(rf'\s{re.escape(attr_name)}="[^"]*"')
18942011
if attr_regex.search(tag):
@@ -3271,6 +3388,13 @@ def get_dir_make_file_and_recurse(directory):
32713388
# Normalize sibling sidebar positions (docs + _category_.json) to avoid UI ordering collisions
32723389
normalize_sidebar_positions_by_parent(DOCS_PREFIX)
32733390

3391+
# Auto-remediate unreadable Mermaid classDef/style color pairs (contrast gate)
3392+
fix_mermaid_diagram_contrast(DOCS_PREFIX)
3393+
if MERMAID_CONTRAST_SUMMARY["scanned"] > 0:
3394+
print("\n### Mermaid diagram contrast analysis ###")
3395+
print(f"Scanned color pairs: {MERMAID_CONTRAST_SUMMARY['scanned']}")
3396+
print(f"Fixed (below {MERMAID_CONTRAST_THRESHOLD}:1): {MERMAID_CONTRAST_SUMMARY['fixed']}")
3397+
32743398
if LOGO_ANALYSIS_SUMMARY["analyzed"] > 0:
32753399
print("\n### Integration logo contrast analysis ###")
32763400
print(f"Analyzed logos: {LOGO_ANALYSIS_SUMMARY['analyzed']}")

src/css/custom.css

Lines changed: 0 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -794,32 +794,6 @@ html[data-theme='dark'] .thin-scrollbar {
794794
scrollbar-color: #333 transparent;
795795
}
796796

797-
/*
798-
* Mermaid diagrams - fix text colors for dark mode
799-
*/
800-
801-
/* Dark mode: Force dark text on mermaid node content (boxes with light backgrounds) */
802-
html[data-theme='dark'] svg .nodeLabel,
803-
html[data-theme='dark'] svg .nodeLabel *,
804-
html[data-theme='dark'] svg .label,
805-
html[data-theme='dark'] svg .label *,
806-
html[data-theme='dark'] svg foreignObject,
807-
html[data-theme='dark'] svg foreignObject *,
808-
html[data-theme='dark'] svg foreignObject strong,
809-
html[data-theme='dark'] svg foreignObject p,
810-
html[data-theme='dark'] svg foreignObject div,
811-
html[data-theme='dark'] svg foreignObject span {
812-
color: #1a1a1a !important;
813-
}
814-
815-
/* Dark mode: Edge labels (connectors) should have light text on dark background */
816-
html[data-theme='dark'] svg .edgeLabel,
817-
html[data-theme='dark'] svg .edgeLabel *,
818-
html[data-theme='dark'] svg .labelBkg,
819-
html[data-theme='dark'] svg .labelBkg * {
820-
color: #e0e0e0 !important;
821-
}
822-
823797
/*
824798
* Search plugin
825799
*/

0 commit comments

Comments
 (0)