Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- // ==UserScript==
- // @name Markdown Live Preview for Mastodon glitch-soc v6.0 (ChatGPT)
- // @version 6.0
- // @namespace http://tampermonkey.net/
- // @description Improved Markdown preview with proper ESC clearing
- // @author Johan
- // @match *://cr8r.gg/*
- // @match *://tilde.zone/*
- // @match *://infosec.exchange/*
- // @match *://toot.cat/*
- // @grant none
- // ==/UserScript==
- (function() {
- 'use strict';
- // 1. Inline-разметка
- function inlineMarkdown(text) {
- return text
- // Ссылки [текст](URL) — улучшенный регексп
- .replace(/\[([^\]]+)\]\((https?:\/\/[^\s)]+)\)/g, '<a href="$2" target="_blank" rel="noopener nofollow noreferrer" class="status-link unhandled-link">$1</a>')
- // .replace(/#([а-яa-z0-9_]+)/gi, '<span href="" class="mention hashtag status-link">#$1</span>')
- .replace( /#([а-яa-z0-9_]+)/gi, (_, tag) => '<a href="' + window.location.origin + '/tags/' + tag + '" class="mention hashtag status-link">#' + tag + '</a>') // теги
- .replace(/@([a-zA-Z0-9_]+)@([a-zA-Z0-9\.\-]+)/g, '<a href="https://$2/@$1" class="u-url mention status-link" rel="nofollow noopener" target="_blank" title="@$1@$2">@<span>$1@$2</span></a>')
- .replace(/\[(.*?)\]\((.*?)\)/g, '<a href="$2" target="_blank">$1</a>')
- .replace(/`(.*?)`/g, '<code>$1</code>')
- .replace(/~~(.*?)~~/g, '<del>$1</del>')
- .replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
- .replace(/__(.*?)__/g, '<strong>$1</strong>')
- .replace(/^#{2,6}\s(.+)$/gm, '<strong>$1</strong>')
- .replace(/\*(.*?)\*/g, '<em>$1</em>')
- .replace(/_(.*?)_/g, '<em>$1</em>')
- }
- // 2. Парсинг Markdown
- function parseMarkdown(raw) {
- const lines = raw.split('\n');
- let result = '';
- let inQuote = false;
- let inCodeBlock = false;
- let codeContent = [];
- let currentParagraph = [];
- let inList = false;
- let listType = '';
- function flushParagraph() {
- if (currentParagraph.length === 0) return;
- let content = currentParagraph.join('\n').trim();
- if (!content) {
- currentParagraph = [];
- return;
- }
- if (inQuote) {
- content = content.replace(/^>\s?/gm, '');
- result += `<blockquote><p>${inlineMarkdown(content)}</p></blockquote>`;
- } else if (listType) {
- const listItems = content.split('\n');
- result += `<${listType === 'unordered' ? 'ul' : 'ol'}>`;
- listItems.forEach(item => {
- const text = item.replace(/^(?:-|\d+\.)\s+/, '').trim();
- if (text) result += `<li>${inlineMarkdown(text)}</li>`;
- });
- result += `</${listType === 'unordered' ? 'ul' : 'ol'}>`;
- } else {
- result += `<p>${inlineMarkdown(content)}</p>`;
- }
- currentParagraph = [];
- }
- function flushCodeBlock() {
- if (codeContent.length === 0) return;
- result += `<pre><code>${codeContent.join('\n')
- .replace(/&/g, '&')
- .replace(/</g, '<')
- .replace(/>/g, '>')
- }</code></pre>`;
- codeContent = [];
- }
- for (const line of lines) {
- const trimmedLine = line.trim();
- if (trimmedLine.startsWith('```')) {
- if (inCodeBlock) {
- flushCodeBlock();
- inCodeBlock = false;
- } else {
- flushParagraph();
- inCodeBlock = true;
- }
- continue;
- }
- if (inCodeBlock) {
- codeContent.push(line);
- continue;
- }
- if (trimmedLine.startsWith('# ')) {
- flushParagraph();
- result += `<h1>${trimmedLine.substring(2)}</h1>`;
- continue;
- }
- const isQuoteLine = line.startsWith('>');
- if (isQuoteLine && !inQuote) {
- flushParagraph();
- inQuote = true;
- } else if (!isQuoteLine && inQuote) {
- flushParagraph();
- inQuote = false;
- }
- const isListLine = /^(-|\d+\.)\s+/.test(trimmedLine);
- if (isListLine && !inList) {
- flushParagraph();
- inList = true;
- listType = trimmedLine.startsWith('-') ? 'unordered' : 'ordered';
- } else if (!isListLine && inList) {
- flushParagraph();
- inList = false;
- listType = '';
- }
- if (trimmedLine === '') {
- flushParagraph();
- continue;
- }
- currentParagraph.push(line);
- }
- flushParagraph();
- flushCodeBlock();
- return result;
- }
- function setupMarkdownPreview(textarea) {
- textarea.style.height = '200px'; // -------------- Здесь высота окна ввода
- textarea.style.minHeight = '200px'; //--------|
- textarea.style.maxHeight = '200px'; //--------|
- let preview = textarea.nextElementSibling;
- // Если превью ещё не создано — создаём
- if (!preview || !preview.classList.contains('md-preview')) {
- preview = document.createElement('div');
- preview.className = 'md-preview status__content__text status__content__text--visible translate';
- preview.style.cssText = `
- border-top: 1px solid #ccc;
- padding: 10px;
- margin-top: 5px;
- white-space: normal;
- overflow-y: auto;
- overflow-x: auto;
- height:440px; //--------------------------- Здесь высота окна просмотра
- overflow-wrap: break-word;
- `;
- textarea.parentNode.insertBefore(preview, textarea.nextSibling);
- }
- // Слушатель ввода: обновляет превью
- textarea.addEventListener('input', () => {
- handleInputEvent(preview);
- preview.innerHTML = parseMarkdown(textarea.value);
- maybeScrollToBottom(preview);
- });
- // Слушатель клавиш: очищает превью при Esc
- textarea.addEventListener('keydown', (e) => {
- if (e.key === 'Escape' && textarea.value === '') {
- handleInputEvent(preview);
- preview.innerHTML = '';
- maybeScrollToBottom(preview);
- // e.preventDefault(); // если нужно — предотврати дефолтное поведение
- }
- });
- // Начальная отрисовка
- preview.innerHTML = parseMarkdown(textarea.value);
- }
- function init() {
- const textarea = document.querySelector('textarea.autosuggest-textarea__textarea');
- if (textarea) {
- setupMarkdownPreview(textarea);
- }
- }
- /*
- // Собственные стили, пока не работает
- const style = document.createElement('style');
- style.textContent = `
- .md-preview {
- font-family: inherit;
- color: inherit;
- }
- .md-preview blockquote {
- border-left: 3px solid #4a90e2 !mportant;
- padding-left: 1em !mportant;
- margin: 0.8em 0 !mportant;
- color: #333 !mportant;
- }
- .md-preview p {
- margin: 0.5em 0 !mportant;
- line-height: 1. !mportant;
- }
- .md-preview code {
- background: #f0f0f0 !mportant;
- padding: 0.2em 0.4em !mportant;
- border-radius: 3px !mportant;
- font-family: monospace !mportant;
- }
- .md-preview pre {
- background: #f5f5f5 !mportant;
- padding: 1em !mportant;
- overflow: auto !mportant;
- border-radius: 3px !mportant;
- }
- .md-preview h1 {
- font-size: 1.2em !mportant;
- margin: 1em 0 0.5em 0 !mportant;
- font-weight: bold !mportant;
- }
- .md-preview ul, .md-preview ol {
- margin: 0.5em 0 !mportant;
- padding-left: 2em !mportant;
- }
- `;
- document.head.appendChild(style);
- */
- // Запуск
- if (document.readyState === 'complete') {
- init();
- } else {
- window.addEventListener('load', init);
- }
- // Проверка, находится ли scroll внизу (или почти внизу)
- function isScrolledToBottom(el) {
- return el.scrollHeight - el.scrollTop <= el.clientHeight + 1;
- }
- // Функция, которая запоминает, был ли scroll внизу
- let wasAtBottom = true; // по умолчанию считаем, что внизу
- function handleInputEvent(viewer) {
- wasAtBottom = isScrolledToBottom(viewer);
- }
- // Функция, которую надо вызывать после обновления контента
- function maybeScrollToBottom(viewer) {
- if (wasAtBottom) {
- viewer.scrollTop = viewer.scrollHeight;
- }
- }
- })();
Advertisement
Add Comment
Please, Sign In to add comment