/// Alignment requested by a TeamSpeak spacer channel tag. enum SpacerAlignment { /// Left-aligned spacer content. left, /// Right-aligned spacer content. right, /// Center-aligned spacer content. center, } /// Built-in TeamSpeak spacer separator patterns. enum SpacerSpecialType { /// `___` solidLine, /// `---` dashLine, /// `...` dotLine, /// `-.-` dashDotLine, /// `-..` dashDotDotLine, } /// Parsed form of a TeamSpeak spacer channel name. class SpacerChannelNameParseResult { /// Construct a spacer parse result. const SpacerChannelNameParseResult({ required this.isSpacer, required this.isValid, required this.alignment, required this.isRepeating, required this.uniqueSuffix, required this.text, required this.specialType, required this.isBlankSpacer, this.reason, }); /// True only when the name begins with a valid bracketed spacer tag. final bool isSpacer; /// True when the bracketed spacer tag follows the supported syntax. final bool isValid; /// Optional alignment flag. Null means the server/client default. final SpacerAlignment? alignment; /// True when `*` appears in the tag and the text should repeat. final bool isRepeating; /// The exact suffix after `Spacer` and before `]`. final String uniqueSuffix; /// The exact text after the closing `]`. final String text; /// Built-in separator type for special text values. final SpacerSpecialType? specialType; /// True for the known blank-looking right-aligned dot spacer. final bool isBlankSpacer; /// Parse error/reason for non-spacer or malformed names. final String? reason; } /// Options for formatting a TeamSpeak spacer channel name. class SpacerChannelNameFormatOptions { /// Construct spacer formatting options. const SpacerChannelNameFormatOptions({ this.alignment, this.isRepeating = false, this.uniqueSuffix = '', this.text = '', }); /// Optional alignment flag. Null means omit the alignment prefix. final SpacerAlignment? alignment; /// Whether to include the repeating `*` tag flag. final bool isRepeating; /// Uniqueness suffix to place after `Spacer`. final String uniqueSuffix; /// Text to place after the closing `]`. final String text; } const _notSpacer = SpacerChannelNameParseResult( isSpacer: false, isValid: false, alignment: null, isRepeating: false, uniqueSuffix: '', text: '', specialType: null, isBlankSpacer: false, reason: 'not a spacer channel name', ); /// Return true when [name] begins with a valid bracketed spacer tag. bool isSpacerChannelName(String name) => parseSpacerChannelName(name).isSpacer; /// Parse a TeamSpeak spacer channel name. /// /// Supported tag form is `[?Spacer#]Text`, parsed case-insensitively. /// `?` may be `l`, `r`, or `c`; `*` may also appear in the tag to /// mark repeating spacer content. The suffix and text are preserved /// exactly as written. SpacerChannelNameParseResult parseSpacerChannelName(String name) { if (!name.startsWith('[')) return _notSpacer; final close = name.indexOf(']'); if (close < 0) { return _invalidSpacerName('missing closing bracket'); } final tag = name.substring(1, close); final text = name.substring(close + 1); final match = RegExp( r'^([lrc*]*)(spacer)(.*)$', caseSensitive: false, ).firstMatch(tag); if (match == null) { if (tag.toLowerCase().contains('spacer')) { return _invalidSpacerName('invalid spacer tag'); } return _notSpacer; } final flags = match.group(1) ?? ''; final uniqueSuffix = match.group(3) ?? ''; final alignmentFlags = flags .toLowerCase() .split('') .where((flag) => flag == 'l' || flag == 'r' || flag == 'c') .toList(); final repeatingFlags = flags.split('').where((flag) => flag == '*').length; if (alignmentFlags.length > 1) { return _invalidSpacerName('multiple alignment flags'); } if (repeatingFlags > 1) { return _invalidSpacerName('multiple repeating flags'); } final alignmentFlag = alignmentFlags.isEmpty ? null : alignmentFlags.first; final alignment = switch (alignmentFlag) { 'l' => SpacerAlignment.left, 'r' => SpacerAlignment.right, 'c' => SpacerAlignment.center, _ => null, }; final specialType = _specialTypeForText(text); return SpacerChannelNameParseResult( isSpacer: true, isValid: true, alignment: alignment, isRepeating: repeatingFlags == 1, uniqueSuffix: uniqueSuffix, text: text, specialType: specialType, isBlankSpacer: alignment == SpacerAlignment.right && text == '.', ); } /// Format a TeamSpeak spacer channel name deterministically. /// /// The output uses canonical `Spacer` casing and places `*` before /// the alignment flag when both are present. String formatSpacerChannelName(SpacerChannelNameFormatOptions options) { final repeatFlag = options.isRepeating ? '*' : ''; final alignFlag = switch (options.alignment) { SpacerAlignment.left => 'l', SpacerAlignment.right => 'r', SpacerAlignment.center => 'c', null => '', }; return '[$repeatFlag${alignFlag}Spacer${options.uniqueSuffix}]${options.text}'; } /// Convert a channel name into display text while preserving the /// underlying channel entity and behavior. String channelSpacerLabel(String raw, {int repeatColumns = 32}) { final parsed = parseSpacerChannelName(raw); if (!parsed.isValid) return raw; if (parsed.isBlankSpacer) return ''; if (parsed.isRepeating) { return _repeatSpacerText(parsed.text, repeatColumns); } return switch (parsed.specialType) { SpacerSpecialType.solidLine => '────────', SpacerSpecialType.dashLine => '╌╌╌╌╌╌╌╌', SpacerSpecialType.dotLine => '········', SpacerSpecialType.dashDotLine => '─╶─╶─╶─╶─╶─╶─╶─╶', SpacerSpecialType.dashDotDotLine => '─╶╶─╶╶─╶╶─╶╶', null => parsed.text, }; } SpacerChannelNameParseResult _invalidSpacerName(String reason) { return SpacerChannelNameParseResult( isSpacer: false, isValid: false, alignment: null, isRepeating: false, uniqueSuffix: '', text: '', specialType: null, isBlankSpacer: false, reason: reason, ); } SpacerSpecialType? _specialTypeForText(String text) { return switch (text) { '___' => SpacerSpecialType.solidLine, '---' => SpacerSpecialType.dashLine, '...' => SpacerSpecialType.dotLine, '-.-' => SpacerSpecialType.dashDotLine, '-..' => SpacerSpecialType.dashDotDotLine, _ => null, }; } String _repeatSpacerText(String pattern, int repeatColumns) { if (pattern.isEmpty) return '────────'; final buffer = StringBuffer(); while (buffer.length < repeatColumns) { buffer.write(pattern); } return buffer.toString().substring(0, repeatColumns); }