244 lines
6.8 KiB
Dart
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);
|
|
}
|