|
// ==UserScript== |
|
// @name copy problems |
|
// @namespace http://tampermonkey.net/ |
|
// @version 2026-05-05 |
|
// @description Добавляет кнопку копирования у вопросов |
|
// @author FlyInk13 |
|
// @match https://open.etu.ru/courses/* |
|
// @grant none |
|
// @icon https://www.google.com/s2/favicons?sz=64&domain=etu.ru |
|
// ==/UserScript== |
|
|
|
(function() { |
|
'use strict'; |
|
|
|
// Функция создания элемента |
|
const ce = (root, type, props = {}) => { |
|
const newNode = document.createElement(type); |
|
if (typeof props === 'string') { |
|
newNode.textContent = props; |
|
} else { |
|
Object.assign(newNode, props); |
|
if (props.cssText) { |
|
newNode.style.cssText = props.cssText; |
|
} |
|
} |
|
return root.appendChild(newNode); |
|
} |
|
|
|
// Функция принимает DOM элемент и отдает его MD версию строкой |
|
const parseProblem = (problem) => { |
|
// Обертки для разных элементов |
|
const wrapByTag = { |
|
'span': (text) => text, |
|
'br': (text) => '\n', |
|
'p': (text) => text + '\n', |
|
'b': (text) => '**' + text + '**', |
|
'tr': (text, el, index) => text + '|\n' + (index ? '' : wrapByTag['tr-header-split'](el)), |
|
'tr-header-split': (el) => '| ---- '.repeat(el.childNodes.length) + '|\n', |
|
'td': (text) => '| ' + text + ' ', |
|
'svg': (text) => '', |
|
'button': (text) => '', |
|
'input-text': (text) => ' _________ ', |
|
'input-radio': (text) => '\n- ', |
|
'input-checkbox': (text) => '\n[ ] ', |
|
'script': (text) => wrapByTag[text.includes('\n') ? 'latex-multiline': 'latex'](text), |
|
'latex': (text) => text.trim() ? ' $' + text + '$ ' : '', |
|
'latex-multiline': (text) => '\n$$\n' + text + '\n$$\n', |
|
}; |
|
|
|
const getText = (el, index) => { |
|
if (el.nodeType === Node.TEXT_NODE) { |
|
return el.textContent.trim(); |
|
} |
|
|
|
const tagName = (el.tagName ?? '').toLocaleLowerCase(); |
|
const wrapName = tagName === 'input' ? [tagName, el.type].join('-') : tagName; |
|
const wrapFn = wrapByTag[wrapName] ?? wrapByTag.span; |
|
// Не показываем текст для скрытых и системных элементов |
|
const isServiceUi = el.classList && (el.classList.contains('status') || el.classList.contains('action') || el.classList.contains('is-hidden')); |
|
if (el.hidden == "hidden" || isServiceUi) { |
|
return ''; |
|
} |
|
|
|
// Получаем текст дочерних элементов |
|
const innerText = Array.from(el.childNodes).map(getText).join(''); |
|
|
|
// Оборачиваем текст в MD символы |
|
return wrapFn(innerText, el, index); |
|
} |
|
|
|
return getText(problem).split(' ').filter(x=>x).join(' '); |
|
} |
|
|
|
// Раз в пол секунды ходим и добавляем вопросам кнопки копирования |
|
const coptProblemTimer = setInterval(() => { |
|
[...document.querySelectorAll('.problem:not([data-copyButtonInserted])')].map((problem) => { |
|
if (problem.dataset.copyButtonInserted) return; |
|
problem.dataset.copyButtonInserted = 1; |
|
|
|
const copyButton = ce(problem.querySelector('.action') ?? problem, 'button', { |
|
textContent: 'Копировать вопрос', |
|
onclick: (event) => { |
|
event.preventDefault(); |
|
Promise.resolve().then(() => { |
|
return parseProblem(problem); |
|
}).then((text) => { |
|
console.log(text); |
|
return navigator.clipboard.writeText(text); |
|
}).then(() => { |
|
copyButton.textContent = 'Скопировано'; |
|
copyButton.style.opacity = '0.2'; |
|
}).catch((error) => { |
|
console.error(error); |
|
copyButton.textContent = 'Ошибка'; |
|
}); |
|
}, |
|
}); |
|
}); |
|
}, 500); |
|
})(); |