/// 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 ''; } /// The project a flow file effectively belongs to: its own /// `project:` slug, or `general` when the file declares none. /// Display/filter semantics only — the FILE stays the truth and /// is never rewritten to make this explicit. String effectiveFlowProject(String fileProject) => fileProject.isEmpty ? 'general' : fileProject; /// Whether a flow file belongs in the list under the active /// workspace filter. Empty [activeProject] = "all projects"; /// otherwise the file's effective project (no key = `general`) /// must match. bool flowVisibleInProject(String fileProject, String activeProject) => activeProject.isEmpty || effectiveFlowProject(fileProject) == activeProject; /// 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; }