import 'dart:convert'; import 'package:html/parser.dart' as html; import '../../../data/json_schedule_import.dart'; import '../../../domain/course.dart'; import '../../../domain/school_identity.dart'; import '../../../domain/school_term.dart'; import 'activity_data.dart'; class UndergraduateTimetableAdapter { static ImportDraft extract( String content, Uri uri, { CourseSource source = CourseSource.school, }) { try { if (uri.scheme != 'https' || uri.host != 'eams.uestc.edu.cn' || uri.port != 443 || uri.userInfo.isNotEmpty || !uri.path.startsWith('/eams/') || source == CourseSource.demo || utf8.encode(content).length > 2 * 1024 * 1024) throw const FormatException('不是本科课表页面'); final doc = html.parse(content), table = html .parse(content) .querySelector('#manualArrangeCourseTable'); if (table == null || doc.querySelector('input[type="password"]') != null) throw const FormatException('请先在学校网页打开课表'); final termIds = doc .querySelectorAll('input[name="semester.id"]') .map((e) => e.attributes['value']) .toSet(); if (termIds.length != 1) throw const FormatException('课表学期不明确'); final term = SchoolTerm( int.parse(termIds.single!), school: SupportedSchool.uestcUndergraduate, ); final scripts = doc .querySelectorAll('script') .map((e) => e.text) .join('\n'); final units = RegExp(r'\b(?:var\s+)?unitCount\s*=\s*(\d+)\s*;') .allMatches(scripts) .map((m) => int.parse(m[1]!)) .toSet(); final activities = units.length == 1 ? parseActivityData(scripts, unitCount: units.single) : []; final explicitEmpty = activities.isEmpty && RegExp(r'暂无课程|无课程安排|没有课程').hasMatch(table.text); if (activities.isEmpty && !explicitEmpty) throw const FormatException('课表尚未加载完整'); final roster = {}; for (final grid in doc.querySelectorAll('table')) { final rows = grid.querySelectorAll('tr'); if (rows.isEmpty) continue; final headers = rows.first.children.map((e) => e.text.trim()).toList(); if (!headers.contains('课程序号') || !headers.contains('课程代码')) continue; final seq = headers.indexOf('课程序号'), code = headers.indexOf('课程代码'), note = headers.indexOf('选课备注'); for (final row in rows.skip(1)) { final cells = row.children; if (cells.length != headers.length) throw const FormatException('课程名单不完整'); roster[cells[seq].text.trim()] = ( code: cells[code].text.trim(), notes: note >= 0 ? cells[note].text.trim() : '', ); } } final cancellations = >{}; for (final a in activities.where((a) => a.cancelled)) { for (final index in a.indices) { cancellations .putIfAbsent('${a.courseCode}/$index', () => {}) .addAll(a.weeks); } } final cells = weeks})>{}; for (final a in activities.where((a) => !a.cancelled)) { for (final index in a.indices) { final weeks = a.weeks.difference( cancellations['${a.courseCode}/$index'] ?? {}, ); if (weeks.isEmpty) continue; final key = jsonEncode([ a.courseCode, a.name, a.teacher, a.room, index, ]); final cell = cells.putIfAbsent( key, () => (a: a, index: index, weeks: {}), ); cell.weeks.addAll(weeks); } } final groups = < String, ({UndergraduateActivity a, Set weeks, List indices}) >{}; for (final cell in cells.values) { final key = jsonEncode([ cell.a.courseCode, cell.a.name, cell.a.teacher, cell.a.room, cell.index ~/ 12, cell.weeks.toList()..sort(), ]); groups .putIfAbsent( key, () => (a: cell.a, weeks: cell.weeks, indices: []), ) .indices .add(cell.index); } final courses = []; for (final group in groups.values) { final indices = group.indices..sort(); for (var p = 0; p < indices.length;) { final first = indices[p]; var last = first; p++; while (p < indices.length && indices[p] == last + 1) { last = indices[p++]; } final a = group.a, info = roster[a.courseCode]; final identity = jsonEncode([ term.id, a.courseCode, a.name, a.teacher, a.room, first, last, group.weeks.toList()..sort(), ]); courses.add( Course( id: 'uestc-undergraduate-${term.id}-${_hash(identity)}', name: a.name, teacher: a.teacher, room: a.room, weekday: first ~/ 12 + 1, startPeriod: first % 12 + 1, endPeriod: last % 12 + 1, weeks: group.weeks, source: source, courseCode: info?.code ?? a.courseCode, notes: info?.notes ?? '', ), ); } } if (courses.isEmpty && !explicitEmpty) throw const FormatException('没有识别到有效上课安排'); return ImportDraft( courses: List.unmodifiable(courses), explicitlyEmpty: explicitEmpty, source: source, parserVersion: 'uestc-undergraduate-v1', schoolTerm: term, semesterLabel: term.label, ); } on FormatException { rethrow; } catch (_) { throw const FormatException('本科课表结构或学期无效,原课表保持不变'); } } static String _hash(String value) { var h = 0x811c9dc5; for (final b in utf8.encode(value)) { h = ((h ^ b) * 0x01000193) & 0xffffffff; } return h.toRadixString(16).padLeft(8, '0'); } }