mirror of
https://github.com/safishamsi/graphify.git
synced 2026-08-27 00:36:39 +00:00
fix(export): stop Obsidian from hiding notes for leading-dot labels
safe_name left stems like .env intact, so the vault wrote .env.md which Obsidian treats as a hidden file — invisible in the explorer and as unresolved wikilinks. Prefix with dot- (shared _obsidian_safe_stem for vault + canvas). True label stays in the note body. Fixes #2205
This commit is contained in:
+33
-30
@@ -429,6 +429,34 @@ def _cap_filename(s: str, limit: int = 200) -> str:
|
||||
return f"{truncated}_{digest}"
|
||||
|
||||
|
||||
def _obsidian_safe_stem(label: str) -> str:
|
||||
"""Filename stem for an Obsidian note / canvas card from a node label.
|
||||
|
||||
Strips filesystem-unsafe characters, a trailing ``.md``-family extension
|
||||
(so ``CLAUDE.md`` does not become ``CLAUDE.md.md``), and a leading ``.`` —
|
||||
Obsidian hides every note whose name starts with a dot, so ``.env.md``
|
||||
would be written but invisible in the UI (#2205). The ``dot-`` prefix keeps
|
||||
the name recognizable; H1 / frontmatter still carry the true label.
|
||||
"""
|
||||
cleaned = re.sub(
|
||||
r'[\\/*?:"<>|#^[\]]',
|
||||
"",
|
||||
label.replace("\r\n", " ").replace("\r", " ").replace("\n", " "),
|
||||
).strip()
|
||||
cleaned = re.sub(r"\.(md|mdx|qmd|markdown)$", "", cleaned, flags=re.IGNORECASE)
|
||||
# Obsidian treats a leading-dot filename as a hidden file (#2205).
|
||||
if cleaned.startswith("."):
|
||||
cleaned = "dot-" + cleaned.lstrip(".")
|
||||
# A stem of only punctuation (e.g. "@", "*", "#") survives the unsafe-char
|
||||
# strip above but is empty once a downstream tool re-slugs on word chars
|
||||
# (e.g. qmd's handelize() reduces "@" -> "" and raises, aborting the whole
|
||||
# `qmd update`). Require at least one word char; else fall back so we never
|
||||
# emit a "@.md"-style filename. (#1409)
|
||||
if not re.search(r"\w", cleaned, flags=re.UNICODE):
|
||||
return "unnamed"
|
||||
return _cap_filename(cleaned)
|
||||
|
||||
|
||||
def _dedup_node_filenames(G: nx.Graph, safe_name) -> dict[str, str]:
|
||||
"""Map each node_id to a unique note filename, appending a numeric suffix on
|
||||
collision. The collision set is keyed on the lowercased name so two labels
|
||||
@@ -497,20 +525,7 @@ def to_obsidian(
|
||||
|
||||
# Map node_id → safe filename so wikilinks stay consistent.
|
||||
# Deduplicate: if two nodes produce the same filename, append a numeric suffix.
|
||||
def safe_name(label: str) -> str:
|
||||
cleaned = re.sub(r'[\\/*?:"<>|#^[\]]', "", label.replace("\r\n", " ").replace("\r", " ").replace("\n", " ")).strip()
|
||||
# Strip trailing .md/.mdx/.markdown so "CLAUDE.md" doesn't become "CLAUDE.md.md"
|
||||
cleaned = re.sub(r"\.(md|mdx|qmd|markdown)$", "", cleaned, flags=re.IGNORECASE)
|
||||
# A stem of only punctuation (e.g. "@", "*", "#") survives the unsafe-char
|
||||
# strip above but is empty once a downstream tool re-slugs on word chars
|
||||
# (e.g. qmd's handelize() reduces "@" -> "" and raises, aborting the whole
|
||||
# `qmd update`). Require at least one word char; else fall back so we never
|
||||
# emit a "@.md"-style filename. (#1409)
|
||||
if not re.search(r"\w", cleaned, flags=re.UNICODE):
|
||||
return "unnamed"
|
||||
return _cap_filename(cleaned)
|
||||
|
||||
node_filename = _dedup_node_filenames(G, safe_name)
|
||||
node_filename = _dedup_node_filenames(G, _obsidian_safe_stem)
|
||||
|
||||
# Helper: compute dominant confidence for a node across all its edges
|
||||
def _dominant_confidence(node_id: str) -> str:
|
||||
@@ -625,7 +640,7 @@ def to_obsidian(
|
||||
community_filename: dict = {}
|
||||
used_community: set[str] = set()
|
||||
for cid in communities:
|
||||
base = f"_COMMUNITY_{safe_name(_community_name(cid))}"
|
||||
base = f"_COMMUNITY_{_obsidian_safe_stem(_community_name(cid))}"
|
||||
candidate = base
|
||||
n = 1
|
||||
while candidate.lower() in used_community:
|
||||
@@ -700,7 +715,7 @@ def to_obsidian(
|
||||
if cross:
|
||||
lines.append("## Connections to other communities")
|
||||
for other_cid, edge_count in sorted(cross.items(), key=lambda x: -x[1]):
|
||||
other_fname = community_filename.get(other_cid) or f"_COMMUNITY_{safe_name(_community_name(other_cid))}"
|
||||
other_fname = community_filename.get(other_cid) or f"_COMMUNITY_{_obsidian_safe_stem(_community_name(other_cid))}"
|
||||
lines.append(f"- {edge_count} edge{'s' if edge_count != 1 else ''} to [[{other_fname}]]")
|
||||
lines.append("")
|
||||
|
||||
@@ -798,21 +813,9 @@ def to_canvas(
|
||||
# Obsidian canvas color codes (cycle through for communities)
|
||||
CANVAS_COLORS = ["1", "2", "3", "4", "5", "6"] # red, orange, yellow, green, cyan, purple
|
||||
|
||||
def safe_name(label: str) -> str:
|
||||
cleaned = re.sub(r'[\\/*?:"<>|#^[\]]', "", label.replace("\r\n", " ").replace("\r", " ").replace("\n", " ")).strip()
|
||||
cleaned = re.sub(r"\.(md|mdx|qmd|markdown)$", "", cleaned, flags=re.IGNORECASE)
|
||||
# A stem of only punctuation (e.g. "@", "*", "#") survives the unsafe-char
|
||||
# strip above but is empty once a downstream tool re-slugs on word chars
|
||||
# (e.g. qmd's handelize() reduces "@" -> "" and raises, aborting the whole
|
||||
# `qmd update`). Require at least one word char; else fall back so we never
|
||||
# emit a "@.md"-style filename. (#1409)
|
||||
if not re.search(r"\w", cleaned, flags=re.UNICODE):
|
||||
return "unnamed"
|
||||
return _cap_filename(cleaned)
|
||||
|
||||
# Build node_filenames if not provided (same dedup logic as to_obsidian)
|
||||
if node_filenames is None:
|
||||
node_filenames = _dedup_node_filenames(G, safe_name)
|
||||
node_filenames = _dedup_node_filenames(G, _obsidian_safe_stem)
|
||||
|
||||
# Fallback: with no community data (e.g. --no-cluster builds or a missing
|
||||
# analysis sidecar) the grid below produces nothing and the canvas is written
|
||||
@@ -926,7 +929,7 @@ def to_canvas(
|
||||
row = m_idx // inner_cols
|
||||
nx_x = gx + 20 + col * (180 + 20)
|
||||
nx_y = gy + 80 + row * (60 + 20)
|
||||
fname = node_filenames.get(node_id, safe_name(G.nodes[node_id].get("label", node_id)))
|
||||
fname = node_filenames.get(node_id, _obsidian_safe_stem(G.nodes[node_id].get("label", node_id)))
|
||||
canvas_nodes.append({
|
||||
"id": f"n_{node_id}",
|
||||
"type": "file",
|
||||
|
||||
Reference in New Issue
Block a user