You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
This repo is archived. You can view files and clone it, but cannot push or open issues/pull-requests.
 
 
 
 
 
 

196 lines
6.1 KiB

export type TaskAssignmentData = {
id: string,
name: string,
points: number | null,
description: string,
titleHtml: string
}
type TaskLocation = {
/** Relative location of HTML file containing this task */
url: string
/** id of the element where the specific task begins */
startElement: string
}
export type TaskStatus = {
id: string
name: string
submitted: boolean
solved: boolean
points: number
maxPoints: number
type: string
}
function fixAllLinks(e: any) {
if (typeof e.src == "string") {
e.src = e.src
}
if (typeof e.href == "string") {
e.href = e.href
}
let c = (e as HTMLElement).firstElementChild
while (c) {
fixAllLinks(c)
c = c.nextElementSibling
}
}
export type ParsedTaskId = {
rocnik: string
z: boolean
serie: string
uloha: string
}
export function parseTaskId(id: string): ParsedTaskId | null {
const m = /^(\d+)-(Z?)(\d)-(\d)$/.exec(id)
if (!m) return null
const [_, rocnik, z, serie, uloha] = m
return { rocnik, z: !!z, serie, uloha }
}
function getLocation(id: string, solution: boolean): TaskLocation {
const parsedId = parseTaskId(id)
if (!parsedId) {
throw new Error("Can not parse " + id)
}
const { rocnik, z, serie, uloha } = parsedId
const urlX = solution ? "reseni" : "zadani"
if (z) {
return {
url: `/z/ulohy/${rocnik}/${urlX}${serie}.html`,
startElement: `task-${id}`
}
} else {
return {
url: `/h/ulohy/${rocnik}/${urlX}${serie}.html`,
startElement: `task-${id}`
}
}
}
function parseTask(startElementId: string, doc: HTMLDocument): TaskAssignmentData {
const titleElement = doc.getElementById(startElementId)
if (!titleElement)
throw new Error(`Document does not contain ${startElementId}`)
fixAllLinks(titleElement)
const elements = []
let e = titleElement
const titleMatch = /^(\d+-Z?\d+-\d+) (.*?)( \((\d+) bod.*\))?$/.exec(e.textContent!.trim())
if (!titleMatch) {
var [_, id, name, __, points] = ["", startElementId, "Neznámé jméno úlohy", "", ""]
} else {
var [_, id, name, __, points] = titleMatch
}
e = e.nextElementSibling as HTMLElement
while (e.nextElementSibling &&
e.tagName.toLowerCase() == "hr")
e = e.nextElementSibling as HTMLElement
while (!e.classList.contains("story") &&
// !e.classList.contains("clearfloat") &&
e.tagName.toLowerCase() != "h3" &&
e.textContent!.trim() != "Řešení"
)
{
elements.push(e)
if (!e.nextElementSibling) break;
e = e.nextElementSibling as HTMLElement
}
// hack: remove img tag that shows this task is a practical one. Some tasks have it, some don't, so we remove it for consistency
const intoImgTag = elements[0]?.firstElementChild
if (intoImgTag && intoImgTag.tagName.toLowerCase() == "img" && intoImgTag.classList.contains("leftfloat")) {
intoImgTag.remove()
}
let r = ""
for (const e of elements) {
// hack: remove the paragraph with the matching text. Occurs in KSP-H, but is useless in this context.
if (e.textContent!.trim().replace(/\s+/g, " ") == "Toto je praktická open-data úloha. V odevzdávacím systému si necháte vygenerovat vstupy a odevzdáte příslušné výstupy. Záleží jen na vás, jak výstupy vyrobíte.") {
continue;
}
fixAllLinks(e)
r += e.outerHTML + "\n"
}
return {
description: r,
id: id.trim(),
name: name.trim(),
points: points ? +points : null,
titleHtml: titleElement.outerHTML
}
}
function parseTaskStatuses(doc: HTMLDocument): TaskStatus[] {
const rows = Array.from(doc.querySelectorAll("table.zs-tasklist tr")).slice(1) as HTMLTableRowElement[]
return rows.map(r => {
const submitted = !r.classList.contains("zs-unsubmitted")
const id = r.cells[0].textContent!.trim()
const type = r.cells[1].textContent!.trim()
const name = r.cells[2].textContent!.trim()
const pointsStr = r.cells[4].textContent!.trim()
const pointsMatch = /((–|\.|\d)+) *\/ *(\d+)/.exec(pointsStr)
if (!pointsMatch) throw new Error()
const points = +pointsMatch[1]
const maxPoints = +pointsMatch[2]
const solved = r.classList.contains("zs-submitted")
return { id, name, submitted, type, points, maxPoints, solved }
})
}
export async function fetchHtml(url: string) {
const r = await fetch(url, { headers: { "Accept": "text/html,application/xhtml+xml" } })
if (r.status >= 400) {
throw Error(r.statusText)
}
const html = await r.text()
const parser = new DOMParser()
const doc = parser.parseFromString(html, "text/html")
if (!doc.head.querySelector("base")) {
let baseEl = doc.createElement('base');
baseEl.setAttribute('href', new URL(url, location.href).href);
doc.head.append(baseEl);
}
return doc
}
async function loadTask({ url, startElement }: TaskLocation): Promise<TaskAssignmentData> {
const html = await fetchHtml(url)
return parseTask(startElement, html)
}
export function isLoggedIn(): boolean {
return !!document.querySelector(".auth a[href='/profil/profil.cgi']")
}
export async function grabTaskStates(kspIds: string[]): Promise<Map<string, TaskStatus>> {
if (!isLoggedIn()) throw new Error()
const ids = new Set<string>(kspIds.map(parseTaskId).filter(t => t != null).map(t => t!.rocnik))
const results = await Promise.all(Array.from(ids.keys()).map(async (rocnik) => {
const html = await fetchHtml(`/cviciste/?year=${rocnik}`)
return parseTaskStatuses(html)
}))
return new Map<string, TaskStatus>(
([] as TaskStatus[]).concat(...results)
.map(r => [r.id, r])
)
}
export async function grabAssignment(id: string): Promise<TaskAssignmentData> {
return await loadTask(getLocation(id, false))
}
export async function grabSolution(id: string): Promise<TaskAssignmentData> {
return await loadTask(getLocation(id, true))
}