Files

244 lines
6.8 KiB
Dart

/// 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);
}