185 lines
6.0 KiB
Dart
185 lines
6.0 KiB
Dart
class UndergraduateActivityException extends FormatException {
|
||
const UndergraduateActivityException(
|
||
this.code,
|
||
String message,
|
||
this.activityNumber,
|
||
) : super(message);
|
||
final String code;
|
||
final int activityNumber;
|
||
}
|
||
|
||
class UndergraduateActivity {
|
||
UndergraduateActivity(
|
||
this.teacher,
|
||
this.name,
|
||
this.courseCode,
|
||
this.room,
|
||
this.indices,
|
||
this.weeks,
|
||
);
|
||
final String teacher, name, courseCode, room;
|
||
final Set<int> indices, weeks;
|
||
bool get cancelled => room == '停课';
|
||
}
|
||
|
||
List<UndergraduateActivity> parseActivityData(
|
||
String script, {
|
||
required int unitCount,
|
||
}) {
|
||
if (unitCount != 12) throw const FormatException('本科节次结构无效');
|
||
final matches = RegExp(
|
||
r'new\s+TaskActivity\s*\((.*?)\)\s*;',
|
||
dotAll: true,
|
||
).allMatches(script).toList();
|
||
if (RegExp(r'\bnew\s+TaskActivity\s*\(').allMatches(script).length !=
|
||
matches.length) {
|
||
throw const FormatException('课程活动声明不完整');
|
||
}
|
||
final result = <UndergraduateActivity>[];
|
||
for (var i = 0; i < matches.length; i++) {
|
||
final m = matches[i], args = _strings(m[1]!);
|
||
if (args.length != 7) throw const FormatException('课程活动字段不完整');
|
||
final suffix = RegExp(r'\(([A-Za-z0-9_.-]+)\)$').firstMatch(args[3]);
|
||
if (suffix == null) {
|
||
throw UndergraduateActivityException(
|
||
'course_sequence_invalid',
|
||
'课程序号格式无法识别,请附带网页反馈',
|
||
i + 1,
|
||
);
|
||
}
|
||
if (args[1].trim().isEmpty) {
|
||
throw UndergraduateActivityException(
|
||
'teacher_missing',
|
||
'课程活动未提供教师信息,请附带网页反馈',
|
||
i + 1,
|
||
);
|
||
}
|
||
if (args[5].trim().isEmpty) {
|
||
throw UndergraduateActivityException(
|
||
'room_missing',
|
||
'课程活动未提供教室信息,请附带网页反馈',
|
||
i + 1,
|
||
);
|
||
}
|
||
final bitmap = args[6];
|
||
if (!RegExp(r'^0[01]+$').hasMatch(bitmap) ||
|
||
bitmap.length > 64 ||
|
||
(bitmap.length > 31 && bitmap.substring(31).contains('1'))) {
|
||
throw const FormatException('上课周次超出1–30周');
|
||
}
|
||
final weeks = {
|
||
for (var w = 1; w < bitmap.length && w <= 30; w++)
|
||
if (bitmap[w] == '1') w,
|
||
};
|
||
if (weeks.isEmpty) throw const FormatException('课程周次为空');
|
||
var end = i + 1 < matches.length ? matches[i + 1].start : script.length;
|
||
final finish = script.indexOf('table0.marshalTable', m.end);
|
||
if (finish >= 0 && finish < end) end = finish;
|
||
final body = script.substring(m.end, end);
|
||
final indices = <int>{};
|
||
final assignments = RegExp(r'\bindex\s*=\s*([^;]+);')
|
||
.allMatches(body)
|
||
.toList();
|
||
final appendPattern = RegExp(
|
||
r'table\d+\.activities\[index\]\s*\[[^;]+?\]\s*=\s*activity\s*;',
|
||
);
|
||
final appends = appendPattern.allMatches(body).length;
|
||
final unconsumed = body
|
||
.replaceAll(appendPattern, '')
|
||
.replaceAll(RegExp(r'\bindex\s*=\s*[^;]+;'), '');
|
||
if (RegExp(r'\btable\d+\.activities\s*\[|\bindex\s*=')
|
||
.hasMatch(unconsumed)) {
|
||
throw const FormatException('存在无法识别的课程节次赋值');
|
||
}
|
||
if (assignments.isEmpty || assignments.length != appends) {
|
||
throw const FormatException('课程节次赋值不完整');
|
||
}
|
||
for (final assignment in assignments) {
|
||
final expression = RegExp(r'^\s*(\d+)\s*\*\s*unitCount\s*\+\s*(\d+)\s*$')
|
||
.firstMatch(assignment[1]!);
|
||
if (expression == null) throw const FormatException('不支持的节次表达式');
|
||
final day = int.parse(expression[1]!), period = int.parse(expression[2]!);
|
||
if (day > 6 || period >= unitCount) {
|
||
throw const FormatException('课程星期或节次无效');
|
||
}
|
||
indices.add(day * unitCount + period);
|
||
}
|
||
result.add(
|
||
UndergraduateActivity(
|
||
args[1].trim(),
|
||
args[3].substring(0, suffix.start).trim(),
|
||
suffix[1]!,
|
||
args[5].trim(),
|
||
indices,
|
||
weeks,
|
||
),
|
||
);
|
||
}
|
||
return result;
|
||
}
|
||
|
||
/// Reads only quoted JS literals; expressions and calls are never evaluated.
|
||
List<String> _strings(String input) {
|
||
final out = <String>[];
|
||
var p = 0;
|
||
void spaces() {
|
||
while (p < input.length && RegExp(r'\s').hasMatch(input[p])) {
|
||
p++;
|
||
}
|
||
}
|
||
|
||
while (true) {
|
||
spaces();
|
||
if (p >= input.length) break;
|
||
final quote = input[p++];
|
||
if (quote != '"' && quote != "'") throw const FormatException('活动参数必须为字符串');
|
||
final b = StringBuffer();
|
||
var closed = false;
|
||
while (p < input.length) {
|
||
var c = input[p++];
|
||
if (c == quote) {
|
||
closed = true;
|
||
break;
|
||
}
|
||
if (c == '\\') {
|
||
if (p >= input.length) throw const FormatException('字符串转义不完整');
|
||
c = input[p++];
|
||
const escapes = {
|
||
'n': '\n',
|
||
'r': '\r',
|
||
't': '\t',
|
||
'b': '\b',
|
||
'f': '\f',
|
||
'v': '\u000b',
|
||
'0': '\u0000',
|
||
};
|
||
if (c == 'u' || c == 'x') {
|
||
final count = c == 'u' ? 4 : 2;
|
||
if (p + count > input.length) throw const FormatException('字符串转义无效');
|
||
final hex = input.substring(p, p + count);
|
||
if (!RegExp(r'^[0-9a-fA-F]+$').hasMatch(hex)) {
|
||
throw const FormatException('字符串转义无效');
|
||
}
|
||
b.writeCharCode(int.parse(hex, radix: 16));
|
||
p += count;
|
||
} else if (escapes.containsKey(c)) {
|
||
b.write(escapes[c]);
|
||
} else if (const ['"', "'", '\\', '/'].contains(c)) {
|
||
b.write(c);
|
||
} else {
|
||
throw const FormatException('不支持的字符串转义');
|
||
}
|
||
} else {
|
||
if (c == '\n' || c == '\r') throw const FormatException('字符串未闭合');
|
||
b.write(c);
|
||
}
|
||
}
|
||
if (!closed) throw const FormatException('字符串未闭合');
|
||
out.add(b.toString());
|
||
spaces();
|
||
if (p == input.length) break;
|
||
if (input[p++] != ',') throw const FormatException('活动字段分隔无效');
|
||
}
|
||
return out;
|
||
}
|