class UndergraduateActivity { UndergraduateActivity( this.teacher, this.name, this.courseCode, this.room, this.indices, this.weeks, ); final String teacher, name, courseCode, room; final Set indices, weeks; bool get cancelled => room == '停课'; } List 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 = []; 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 || args[1].trim().isEmpty || args[5].trim().isEmpty) { throw const FormatException('教师、教室或课程序号缺失'); } 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 = {}; 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 _strings(String input) { final out = []; 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; }