Skip to content

Instantly share code, notes, and snippets.

@FlyInk13
Last active May 28, 2026 13:53
Show Gist options
  • Select an option

  • Save FlyInk13/2161f076fb36b570c113dbbcb2492141 to your computer and use it in GitHub Desktop.

Select an option

Save FlyInk13/2161f076fb36b570c113dbbcb2492141 to your computer and use it in GitHub Desktop.
Добавляет кнопку копирования у вопросов на сайте open.etu.ru
// ==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);
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment