/// Parsing + normalization for a flow file's own top-level /// `project:` key. Kept separate from the page so the "file wins" /// semantics are unit-testable without pumping the whole editor. /// /// Mirrors the hub's `resolve_project` (file's `project:` is the /// run's project unless the caller overrides it) and /// `normalize_project_slug`, so the chip the editor shows compares /// equal to what the hub actually stamps. library; /// The flow's own top-level `project:` slug (normalized), or empty /// when the YAML declares none. Only a *non-indented* `project:` /// line counts — a nested `project:` inside a step's `with:` block /// is ignored. String parseFlowProject(String yaml) { for (final raw in yaml.split('\n')) { if (raw.startsWith(' ') || raw.startsWith('\t')) continue; final m = RegExp(r'^project:\s*(.+?)\s*$').firstMatch(raw); if (m != null) { final value = m.group(1)!.replaceAll(RegExp('''^["']|["']\$'''), ''); return normalizeProjectSlug(value); } } return ''; } /// Mirror of the hub's `normalize_project_slug`: lowercase, /// collapse spaces/underscores/dashes to a single `-`, drop /// everything else, trim trailing dashes. Empty in → empty out /// (the editor treats "no project" and "blank project" alike). String normalizeProjectSlug(String raw) { final buf = StringBuffer(); var prevDash = false; for (final ch in raw.trim().toLowerCase().split('')) { if (RegExp(r'[a-z0-9]').hasMatch(ch)) { buf.write(ch); prevDash = false; } else if ((ch == ' ' || ch == '_' || ch == '-') && !prevDash && buf.isNotEmpty) { buf.write('-'); prevDash = true; } } var out = buf.toString(); while (out.endsWith('-')) { out = out.substring(0, out.length - 1); } return out; }