feat: sync 12 new skills into catalog (total 31 skills), update manifest and README
This commit is contained in:
205
skills/drawio-skill/scripts/aiicons.py
Normal file
205
skills/drawio-skill/scripts/aiicons.py
Normal file
@@ -0,0 +1,205 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Find AI / LLM brand logos (OpenAI, Claude, Gemini, ...) as draw.io styles.
|
||||
|
||||
draw.io's bundled shape libraries have no modern AI/LLM brand logos, so an
|
||||
"LLM app architecture" renders as generic boxes. This resolves a brand name to a
|
||||
draw.io `image` style that references the matching SVG from the lobe-icons set
|
||||
(https://github.com/lobehub/lobe-icons, MIT) on the unpkg CDN.
|
||||
|
||||
python3 aiicons.py "openai"
|
||||
python3 aiicons.py "claude" --json
|
||||
python3 aiicons.py "langchain" --variant mono --size 48
|
||||
|
||||
The icon is referenced by URL (data/lobe-icons.json carries only the name list,
|
||||
not the assets), so draw.io fetches it from the CDN when the diagram is rendered
|
||||
or opened. That means **network is required at render time**; an offline export
|
||||
draws a blank box. Use --embed to fetch the SVG once and inline it as a
|
||||
self-contained data URI instead (portable, no network at render time).
|
||||
|
||||
The logos are trademarks of their respective owners and are referenced here for
|
||||
identification only — the same basis on which draw.io ships AWS/Azure icons.
|
||||
|
||||
Usage: python3 aiicons.py <query> [--limit N] [--variant color|mono|text]
|
||||
[--size PX] [--embed] [--json] [--list]
|
||||
"""
|
||||
import argparse
|
||||
import base64
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import urllib.request
|
||||
|
||||
MANIFEST = os.path.join(os.path.dirname(__file__), "..", "data", "lobe-icons.json")
|
||||
STYLE = ("shape=image;html=1;imageAspect=0;aspect=fixed;"
|
||||
"verticalLabelPosition=bottom;verticalAlign=top;image=")
|
||||
_VARIANT = re.compile(r"-(?:color|text(?:-[a-z]{2})?|brand(?:-color)?)$")
|
||||
|
||||
# Common RAG/LLM data stores that lobe-icons lacks, mapped to simple-icons
|
||||
# slugs (https://simpleicons.org, CC0). Served from the simple-icons CDN. Each
|
||||
# slug below is verified to return HTTP 200 at https://cdn.simpleicons.org/<slug>.
|
||||
_SIMPLEICONS_CDN = "https://cdn.simpleicons.org/"
|
||||
_SUPPLEMENT = {
|
||||
"qdrant": "qdrant",
|
||||
"milvus": "milvus",
|
||||
"supabase": "supabase",
|
||||
"redis": "redis",
|
||||
"postgresql": "postgresql",
|
||||
"mongodb": "mongodb",
|
||||
"elasticsearch": "elasticsearch",
|
||||
"neo4j": "neo4j",
|
||||
"kafka": "apachekafka",
|
||||
"clickhouse": "clickhouse",
|
||||
"duckdb": "duckdb",
|
||||
"mysql": "mysql",
|
||||
"sqlite": "sqlite",
|
||||
"cassandra": "apachecassandra",
|
||||
"snowflake": "snowflake",
|
||||
"databricks": "databricks",
|
||||
"mariadb": "mariadb",
|
||||
"couchbase": "couchbase",
|
||||
}
|
||||
|
||||
|
||||
def families(icons):
|
||||
"""base brand name -> set of its variant filenames (without .svg)."""
|
||||
fam = {}
|
||||
for name in icons:
|
||||
base = _VARIANT.sub("", name)
|
||||
fam.setdefault(base, set()).add(name)
|
||||
return fam
|
||||
|
||||
|
||||
def squish(s):
|
||||
return re.sub(r"[^a-z0-9]", "", s.lower())
|
||||
|
||||
|
||||
def search(fam, query, limit):
|
||||
"""Rank brand bases against the query (squished + per-token matching)."""
|
||||
q = squish(query)
|
||||
tokens = [t for t in re.findall(r"[a-z0-9]+", query.lower()) if t]
|
||||
scored = {}
|
||||
for base in fam:
|
||||
b = squish(base)
|
||||
s = 0
|
||||
if q and q == b:
|
||||
s = 100
|
||||
elif q and b.startswith(q):
|
||||
s = 60
|
||||
elif q and q in b:
|
||||
s = 40
|
||||
for t in tokens:
|
||||
if t == b:
|
||||
s = max(s, 90)
|
||||
elif len(t) >= 3 and b.startswith(t):
|
||||
s = max(s, 50)
|
||||
elif len(t) >= 3 and t in b:
|
||||
s = max(s, 30)
|
||||
if s:
|
||||
scored[base] = s
|
||||
return sorted(scored, key=lambda base: (-scored[base], base))[:limit]
|
||||
|
||||
|
||||
def search_supplement(query):
|
||||
"""Fall back to the simple-icons supplement (exact or substring match)."""
|
||||
q = squish(query)
|
||||
if not q:
|
||||
return None
|
||||
if q in _SUPPLEMENT:
|
||||
return q
|
||||
for brand in _SUPPLEMENT:
|
||||
if q in brand or brand in q:
|
||||
return brand
|
||||
return None
|
||||
|
||||
|
||||
def pick_variant(base, variants, prefer):
|
||||
order = {"color": ["-color", "-brand-color", "", "-brand", "-text", "-text-cn"],
|
||||
"mono": ["", "-brand", "-color", "-brand-color", "-text", "-text-cn"],
|
||||
"text": ["-text", "-text-cn", "-brand", "-brand-color", "-color", ""]}[prefer]
|
||||
for suffix in order:
|
||||
cand = base + suffix
|
||||
if cand in variants:
|
||||
return cand
|
||||
return next(iter(sorted(variants)), None)
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(description="Find AI/LLM brand logos as draw.io styles (lobe-icons via CDN).")
|
||||
ap.add_argument("query", nargs="?", help='brand name, e.g. "openai" or "claude"')
|
||||
ap.add_argument("--limit", type=int, default=8)
|
||||
ap.add_argument("--variant", choices=["color", "mono", "text"], default="color")
|
||||
ap.add_argument("--size", type=int, default=48, help="cell width/height in px (icons are square)")
|
||||
ap.add_argument("--embed", action="store_true",
|
||||
help="inline the SVG as a data URI (fetches it now; portable, no network at render time)")
|
||||
ap.add_argument("--json", action="store_true")
|
||||
ap.add_argument("--list", action="store_true", help="list all brand names and exit")
|
||||
args = ap.parse_args()
|
||||
|
||||
if not os.path.exists(MANIFEST):
|
||||
sys.exit(f"error: manifest not found at {MANIFEST}")
|
||||
with open(MANIFEST, encoding="utf-8") as f:
|
||||
manifest = json.load(f)
|
||||
fam = families(manifest["icons"])
|
||||
cdn = manifest["cdn"]
|
||||
|
||||
if args.list:
|
||||
for base in sorted(fam):
|
||||
print(base)
|
||||
return
|
||||
if not args.query:
|
||||
ap.error("a query is required (or use --list)")
|
||||
|
||||
matches = search(fam, args.query, args.limit)
|
||||
|
||||
results = []
|
||||
if matches:
|
||||
for base in matches:
|
||||
file = pick_variant(base, fam[base], args.variant)
|
||||
url = f"{cdn}{file}.svg"
|
||||
if args.embed:
|
||||
try:
|
||||
svg = urllib.request.urlopen(url, timeout=15).read()
|
||||
except Exception as exc: # noqa: BLE001 - report and skip
|
||||
sys.stderr.write(f"warning: could not fetch {url} ({exc})\n")
|
||||
continue
|
||||
# Rewrite the 1em intrinsic size so draw.io scales the inlined SVG.
|
||||
svg = svg.replace(b'width="1em"', b'width="24"').replace(b'height="1em"', b'height="24"')
|
||||
# Marker-less base64: draw.io splits style values on ';', so a
|
||||
# ';base64,' marker would truncate the image= value (issue #80).
|
||||
image = "data:image/svg+xml," + base64.b64encode(svg).decode()
|
||||
else:
|
||||
image = url
|
||||
results.append({"brand": base, "file": file, "w": args.size, "h": args.size,
|
||||
"style": STYLE + image})
|
||||
else:
|
||||
# lobe has no logo for this brand; fall back to the simple-icons supplement.
|
||||
brand = search_supplement(args.query)
|
||||
if brand:
|
||||
slug = _SUPPLEMENT[brand]
|
||||
url = _SIMPLEICONS_CDN + slug
|
||||
image = url
|
||||
if args.embed:
|
||||
try:
|
||||
svg = urllib.request.urlopen(url, timeout=15).read()
|
||||
# Marker-less base64 (see issue #80 note above).
|
||||
image = "data:image/svg+xml," + base64.b64encode(svg).decode()
|
||||
except Exception as exc: # noqa: BLE001 - keep the CDN URL
|
||||
sys.stderr.write(f"warning: could not fetch {url} ({exc}); using CDN URL\n")
|
||||
results.append({"brand": brand, "file": f"simpleicons:{slug}",
|
||||
"w": args.size, "h": args.size, "style": STYLE + image})
|
||||
|
||||
if not results:
|
||||
sys.exit(f"no logo for {args.query!r} — for a data store try a cylinder "
|
||||
f"(shape=cylinder3) or shapesearch.py '{args.query} database'")
|
||||
|
||||
if args.json:
|
||||
print(json.dumps(results, indent=2, ensure_ascii=False))
|
||||
else:
|
||||
for r in results:
|
||||
shown = r["style"] if len(r["style"]) < 160 else r["style"][:157] + "..."
|
||||
print(f"{r['brand']} ({r['file']}, {r['w']}x{r['h']})\n {shown}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user