[北京理工大学 BIT] 乐学增强脚本整合包

乐学脚本儿大礼包

// ==UserScript==
// @name         [北京理工大学 BIT] 乐学增强脚本整合包
// @namespace    http://tampermonkey.net/
// @version      0.2.3
// @description  乐学脚本儿大礼包
// @license      GPL-3.0-or-later
// @supportURL   https://github.com/windlandneko/
// @author       Charlie, Y.D.X.
// @match        https://lexue.bit.edu.cn/*
// @grant        none
// ==/UserScript==

// document.querySelector('.header-main > .container').remove()

if (location.href.includes('submit.php'))
    // submit.php 自动点击 查看结果 按钮
    onload = () => document.querySelector('a[href^=result]').click()
;(() => {
    // * 删除姓名中的空格
    const selectors = [
        "a[href*='/user/view.php'], #page-navbar a[href*='/user/profile.php']", // 通用
        '.usertext', // 通用:header 中头像的左边
        '.fullname', // 首页 - 已登录用户
        'author-info > .text-truncate', // forum/view.php
        '#page-header h1, head title', // user/profile.php
        '#page-content .userprofile .page-header-headings > h2', // user/view.php
    ]

    function format(str) {
        return str.match(/[a-zA-Z]/) ? str : str.replaceAll(' ', '')
    }

    // assign/view.php
    // 整理评分人名字这一格的格式。
    if (document.querySelector('.feedback table.generaltable')) {
        const cell_gradedBy = document.querySelector(
            '.feedback table.generaltable > tbody > tr:last-child > td:last-child'
        )
        const user_url = cell_gradedBy.querySelector('a').href
        const user_name = cell_gradedBy.textContent
        cell_gradedBy.innerHTML = `<a href="${user_url}">${user_name}</a >`
    }

    // grade/report/(overview|user)/index.php
    if (
        location.pathname.match(/^\/grade\/report\/(overview|user)\/index\.php$/)
    ) {
        const headline = document.querySelector('#maincontent + h2')
        if (headline) {
            headline.textContent = headline.textContent.replace(
                /^(..报表) - (.+)$/,
                (_, prefix, name) => prefix + ' - ' + format(name)
            )
        }
    }

    document.querySelectorAll(selectors.join(', ')).forEach(el => {
        el.textContent = format(el.textContent)
    })
})()
;(() => {
    if (!location.href.includes('result.php')) return
    // result.php 结果页自动刷新

    addEventListener('load', () => {
        const titles = document.querySelectorAll('#region-main h3')
        if (
            titles.length <= 1 ||
            !titles[titles.length - 1].textContent.includes('测试结果')
        )
            setTimeout(() => location.reload(), 1000)
    })
})()
;(() => {
    if (!location.href.includes('result.php')) return
    // result.php 设置测试点颜色

    for (const row of document.querySelectorAll(
        '#test-result-detail > table > tbody > tr'
    )) {
        const result = row.querySelector('.cell.c12').textContent
        let color

        if (result.includes('RE:')) color = 'LightBlue'
        if (result.includes('FPE:')) color = 'BlanchedAlmond'
        if (result.includes('TLE:')) color = 'Tomato'
        if (result.includes('KS:')) color = 'Violet'

        if (color)
            for (const column of row.querySelectorAll('.cell'))
                column.style.backgroundColor = color
    }
})()
;(() => {
    if (!location.href.includes('view.php')) return
    // view.php 自动折叠公告

    function add_style_sheet() {
        const style = document.createElement('style')
        style.textContent = `\
      [role=main] > .course-content > .collapse-content {
          height: 15em;
          overflow: auto;
      }

      .hider {
          margin-top: -4em;
          background: linear-gradient(#0000, lightgray);
          height: 4em;

          display: grid;
          place-content: center;
      }
      #show-all {
          z-index: 2;
      }
      `
        document.head.appendChild(style)
    }

    const course_content = document.querySelector('[role=main] > .course-content')
    if (!course_content) return
    const front_content = course_content.querySelector(
        '.course-content > ul:first-child'
    )
    const single_section = course_content.querySelector('.single-section')
    const collapse = 'collapse-content' // moodle 已经占用了 .collapse:not(.show)

    if (front_content.clientHeight > (1 / 3) * window.innerHeight) {
        add_style_sheet()

        front_content.querySelectorAll('h1, h2, h3, h4, h5, h6').forEach(h => {
            h.textContent = h.textContent.trim()
        })
        front_content.classList.add(collapse)
        const div = document.createElement('div')
        div.classList.add('hider')
        div.innerHTML = '<button id="show-all">▼展开</button>'
        course_content.insertBefore(div, single_section)

        div.querySelector('#show-all').addEventListener('click', () => {
            front_content.classList.remove(collapse)
            div.hidden = true
        })
    }
})()
;(() => {
    if (!location.href.includes('view.php')) return
    // view.php 题目状态颜色显示

    function add_style_sheet() {
        const sheet = document.createElement('style')
        sheet.innerHTML = `\
      .problem-state-true,
      .quiz-state-true
      {
          color: #52C41A;
      }

      .problem-state-null,
      .quiz-state-false
      .problem-state-false
      {
          font-weight: bold;
          color: #E74C3C;
      }

      .problem-state-undefined
      {
          color: purple;
      }

      .state-pending {
          animation: pending 2s infinite ease;
      }
      .state-fetcherror {
          border: 3px solid red;
      }

      @keyframes pending {
          0% { background-color: #eee; }
          50% { background-color: #ddd; }
          100% { background-color: #eee; }
      }
      `
        document.head.appendChild(sheet)
    }

    async function check_all_problems() {
        for await (const e of document.querySelectorAll(
            "#region-main a[href*='lexue.bit.edu.cn/mod/programming/view']"
        )) {
            e.classList.add('state-pending')
            const result = await get_problem_state(
                new URL(e.href).searchParams.get('id')
            ).catch(err => e.classList.add('state-fetcherror'))
            e.classList.remove('state-pending')
            e.classList.add('quiz-state-' + String(result))
        }
    }
    async function check_all_quizzes() {
        for await (const e of document.querySelectorAll(
            "#region-main a[href*='lexue.bit.edu.cn/mod/quiz/view']"
        )) {
            e.classList.add('state-pending')
            const result = await get_quiz_state(e.href).catch(err =>
                                                              e.classList.add('state-fetcherror')
                                                             )
            e.classList.remove('state-pending')
            e.classList.add('problem-state-' + String(result))
        }
    }

    const parser = new DOMParser()

    /**
   * 获取编程题的情况
   * @param {String} problem_id 编程题的 id
   * @returns null 尚未提交
   * @returns undefined 正在排队或正在编译
   * @returns true 全部通过
   * @returns false 已提交但尚未全部通过
   */
    async function get_problem_state(problem_id) {
        const response = await fetch(
            document.location.origin + `/mod/programming/result.php?id=${problem_id}`
        ).then(e => e.text())
        const html = parser.parseFromString(response, 'text/html')

        const headings = html.querySelectorAll('#region-main h3')
        if (headings.length <= 1) return null
        if (headings[headings.length - 1].textContent !== '测试结果')
            return undefined

        const result = html.querySelector(
            '#test-result-detail > p:first-child'
        ).textContent
        const data = result.match(
            /测试结果:共 (?<total>\d+) 个测试用例,您的程序通过了其中的 (?<accepted>\d+) 个,未能通过的有 (?<rejected>\d+) 个。/
        )
        return data.groups.rejected == 0
    }

    /**
   * 获取测验的情况
   * @param {String} href 测验的 URL
   * @returns true 已完成
   * @returns false 未完成
   */
    async function get_quiz_state(href) {
        const response = await fetch(href).then(e => e.text())
        const heading = parser
        .parseFromString(response, 'text/html')
        .querySelector('[role=main] > h3')
        return heading && heading.textContent == '您上次尝试的概要'
    }

    add_style_sheet()
    check_all_problems()
    check_all_quizzes()
})()
;(async () => {
    if (!location.href.includes('mod/programming')) return
    // mod/programming 代码展示增强

    const style = document.createElement('style')
    style.textContent = `\
.path-mod-programming td.programming-io li::marker { content: ''; }
.path-mod-programming td.programming-io li {
  font-family: 'Fira Code', 'Courier New', Courier, monospace;
  line-height: normal;
}

pre.custom-pre {
margin: 0.5em 0;
padding: 0.3em 0.5em;
border: #ddd solid 1px;
background: #f8f8f8;
border-radius: 3px;
overflow: auto;
font-size: 0.875em;
font-family: monospace;
}
pre.custom-pre > div {
  overflow: hidden !important;
  height: unset !important;
  width: unset !important;
}
pre.custom-pre .ret {
  background: none !important;
  user-select: none !important;
  opacity: 0.3;
}

textarea#id_code {
  width: inherit;
  font-family: monospace;
}

.dp-highlighter ol {
  counter-reset: count;
}
.dp-highlighter ol li {
  line-height: unset;
  counter-increment: count;
  background-color: #f8f8f8 !important;
}
.dp-highlighter ol li::marker {
  content: counter(count, decimal-leading-zero) " ";
}
.path-mod-programming #codeview {
  overflow: auto;
}

button.copy-button {
  background-color: #4285F4;
  color: #ffffff;
  border-radius: 4px;
  margin: 0.2em 0.8em;
  padding: 0.2em 0.5em;
  font-size: 0.8em;
  font-family: initial;
  min-height: auto;
  line-height: normal;

  transition: background-color 150ms ease;
}
button.copy-button:hover {
  background-color: #669DF6;
}
button.copy-button:focus {
  background-color: #4285F4;
}
button.copy-button:active {
  background-color: #2A4D80;
}

.testcase-table tbody tr {
  display: flex;
}
.testcase-table tbody tr > td {
  flex: 1;
  background-color: #ffffff !important;
}
`
    document.head.appendChild(style)

    // IndexedDB Cache
    const dbName = 'lexueCache'
    const storeName = 'sampleCodeCache'

    async function dropDB() {
        return new Promise((res, rej) => {
            const request = indexedDB.deleteDatabase(dbName)
            request.onsuccess = res
            request.onerror = rej
        })
    }

    const version = localStorage.getItem('lexue-patch-version')
    if (parseInt(version) < 1) await dropDB()
    localStorage.setItem('lexue-patch-version', 1)

    async function openDB() {
        // drop the database first

        return new Promise((res, rej) => {
            const request = indexedDB.open(dbName)

            request.onsuccess = event => res(event.target.result)
            request.onerror = event => rej(event.target.error)

            request.onupgradeneeded = event => {
                const db = event.target.result
                if (!db.objectStoreNames.contains(storeName))
                    db.createObjectStore(storeName, { keyPath: 'key' })
            }
        })
    }

    async function getCache(key) {
        return new Promise(async (res, rej) => {
            const db = await openDB()
            const store = db.transaction(storeName, 'readonly').objectStore(storeName)
            const request = store.get(key)

            request.onsuccess = event => res(event.target.result ? event.target.result.code : null)
            request.onerror = event => rej(event.target.error)
        })
    }

    async function setCache(key, code) {
        return new Promise(async (res, rej) => {
            const db = await openDB()
            const store = db
            .transaction(storeName, 'readwrite')
            .objectStore(storeName)
            const request = store.put({ key, code })

            request.onsuccess = () => res()
            request.onerror = event => rej(event.target.error)
        })
    }

    if (document.querySelector('.testcase-table')) {
        document.querySelectorAll('.testcase-table .c0').forEach(e => e.remove())
        document.querySelector('.testcase-table thead').style.display = 'none'
    }
    const is3column = document.querySelector('#test-result-detail')
    document
        .querySelectorAll("td.programming-io > a[href*='download=0']")
        .forEach(async (a, id) => {
        // Description
        const description = document.createElement('span')
        description.textContent = is3column
            ? `${['输入', '答案', '输出'][id % 3]} #${~~(id / 3) + 1}`
            : `${['输入',        '输出'][id % 2]} #${~~(id / 2) + 1}`
        description.style.fontWeight = 'bold'
        description.style.backgroundColor = 'unset'

        // Copy button
        const copy_button = document.createElement('button')
        copy_button.textContent = '复制'
        copy_button.classList.add('copy-button')

        // Code block
        const pre = document.createElement('pre')
        pre.classList.add('custom-pre')
        pre.style.color = '#bbbbbb'
        pre.textContent = '(获取中)'

        a.parentNode.prepend(description, copy_button)
        a.parentNode.replaceChild(pre, a.parentNode.lastChild)
        a.remove()

        const key = new URL(a.href).search
        let str = await getCache(key)
        if (!str) {
            const data = await fetch(a.href).then(res => res.text())
            await setCache(key, data)
            str = data
        }

        if (str.length == 0) pre.textContent = '(输入为空)'
        else pre.textContent = '', pre.style.color = 'inherit'

        str = str.replace(/\r/g, '')
        let hasTrailingLineBreak = str[str.length - 1] == '\n'

        str.split('\n').forEach((line, lineNo, lineArray) => {
            // 末尾有换行,且在最后一行,那么省略渲染最后一个空行
            if(hasTrailingLineBreak && lineNo == lineArray.length - 1) return

            const div = document.createElement('div')
            pre.appendChild(div)

            const code = document.createElement('code')
            code.textContent = line
            div.append(code)

            // 末尾无换行,且在最后一行,那么不渲染换行符
            if(!hasTrailingLineBreak && lineNo == lineArray.length - 1) return

            const linebreak = document.createElement('span')
            linebreak.textContent = '↵'
            linebreak.classList.add('ret')
            div.append(linebreak)
        })

        copy_button.addEventListener('click', () =>
                                     navigator.clipboard.writeText(str).then(
            () => {
                if (copy_button.textContent == '复制')
                    setTimeout(() => (copy_button.textContent = '复制'), 1000)
                copy_button.textContent = '复制成功'
            },
            err => {
                if (copy_button.textContent == '复制')
                    setTimeout(() => (copy_button.textContent = '复制'), 2000)
                copy_button.textContent = '复制失败'
            }
        )
                                    )
    })
})()
;(() => {
    if (!location.href.includes('result.php')) return
    // result.php 显示优化
    if (!document.querySelector('#test-result-detail')) return
    if(document.querySelector('.compilemessage'))
        document.querySelector('.compilemessage').style.backgroundColor = '#ffe4e4'
    const el = document.querySelector('#test-result-detail').previousSibling
    el.style.fontWeight = 'bold'
    el.style.backgroundColor = '#ddd'
    el.style.margin = '0.5em 0'
    el.style.padding = '0.2em 0.3em'
    el.style.borderRadius = '4px'
    const text = document.querySelector(
        '#test-result-detail > p:first-child'
    ).textContent
    const data = text.match(
        /测试结果:共 (?<total>\d+) 个测试用例,您的程序通过了其中的 (?<accepted>\d+) 个,未能通过的有 (?<rejected>\d+) 个。/
    )
    if (data) {
        const { total, accepted } = data.groups
        el.textContent += ` (${accepted} / ${total})`
        if (accepted == total) {
            el.textContent += ' Accepted!'
            el.style.color = '#fff'
            el.style.backgroundColor = '#42ab0e'
        } else {
            el.textContent += ' Wrong Answer.'
            el.style.backgroundColor = '#ffe4e4'
            el.style.color = '#ff0424'
        }
    }
    document.querySelectorAll('#test-result-detail > p').forEach(e => e.remove())
})()
;(() => {
    if (!location.href.includes('history.php')) return
    // history.php 显示优化

    const update = () => {
        const parser = new DOMParser()
        const el = document.querySelector('.dp-highlighter')
        const code = parser.parseFromString(
            `<div>${el.highlighter.originalCode}</div>`,
            'text/html'
        ).body.textContent

        const copy_button = document.createElement('button')
        copy_button.textContent = '复制'
        copy_button.classList.add('copy-button')
        document.querySelector('.dp-highlighter .tools').replaceWith(copy_button)
        copy_button.addEventListener('click', () =>
                                     navigator.clipboard.writeText(code).then(
            () => {
                if (copy_button.textContent == '复制')
                    setTimeout(() => (copy_button.textContent = '复制'), 1000)
                copy_button.textContent = '复制成功'
            },
            err => {
                if (copy_button.textContent == '复制')
                    setTimeout(() => (copy_button.textContent = '复制'), 2000)
                copy_button.textContent = '复制失败'
            }
        )
                                    )
    }

    document
        .querySelectorAll('#submitlist')
        .forEach(el => el.addEventListener('click', () => setTimeout(update, 100)))
    addEventListener('load', update)
})()