You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
A control character in a label aborts the whole GraphML and Obsidian export (ANSI escape in a heading, form feed in source) #2897
A C0 control character in a node label aborts two exporters outright. The character arrives from ordinary corpus content, passes detection, extraction and the graph build without complaint, and then costs the entire export.
to_graphml raises ValueError: All strings must be XML compatible: Unicode or ASCII, no NULL bytes or control characters — XML 1.0 permits tab, LF and CR and no other C0 control.
to_obsidian raises OSError: [Errno 22] Invalid argument on Windows, which rejects control characters in a path outright. One bad label costs the whole vault, not one note.
Reproduce
One markdown file whose heading carries an ANSI colour sequence — what you get by pasting a terminal capture into docs:
nodes=5 labels with a C0 control char: 1
'Build \x1b[31mlog\x1b[0m capture'
graphml CRASH ValueError: All strings must be XML compatible: Unicode or ASCII, no NULL bytes or control characters
cypher OK
json OK
obsidian CRASH OSError: [Errno 22] Invalid argument
html OK
ANSI escapes (\x1b) appear whenever a terminal capture is committed into a .md/.txt and a heading includes coloured output.
Form feed (\x0c) is a long-standing Emacs/Python convention for a page separator inside source files.
Neither is exotic, and nothing earlier in the pipeline objects. The failure surfaces only at the very end, after the expensive work is done.
The codebase already knows
This reads as a gap rather than a deliberate choice, because the other paths handle it:
_cypher_escape explicitly drops C0 controls — its docstring says "First normalise: drop NUL and other C0 control chars except tab."
to_html routes labels through security.sanitize_label, whose docstring is "Strip control characters and cap length."
Only to_graphml and to_obsidian pass the raw label straight through to a writer that cannot take it.
Suggested fix
Sanitise at those two boundaries, matching what the other exporters already do:
GraphML — filter strings in the existing _graphml_safe coercion hook (the one added for the None/non-scalar cases in to_graphml crashes on dict/list-valued attributes (node metadata, graph-level hyperedges) #1831), dropping only the C0 controls XML forbids so tab/LF/CR survive. Node IDs need the same treatment: they become the id attribute of every <node> and edge endpoint, so a control character there is equally fatal.
Obsidian — _obsidian_safe_stem already folds CR/LF to spaces; folding the remaining C0 controls (and DEL) the same way keeps them out of a path Windows rejects.
Losing an escape character is the right cost here. Losing the export is not.
I have this working with tests and will open a PR shortly.
Activity
fix(export): a control character in a label must not abort the export (…
docs(changelog): add 0.9.48 entries for #2897, #2853, #2881
chore: bump to 0.9.48
safishamsi commentedon Aug 20, 2026
Fixed in v0.9.48: https://github.com/Graphify-Labs/graphify/releases/tag/v0.9.48 (a control character in a label no longer aborts the GraphML/Obsidian export). Closing as resolved.