Guest User

Untitled

a guest
Jul 23rd, 2025
208
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 9.34 KB | None | 0 0
  1. // ==UserScript==
  2. // @name Markdown Live Preview for Mastodon glitch-soc v6.0 (ChatGPT)
  3. // @version 6.0
  4. // @namespace http://tampermonkey.net/
  5. // @description Improved Markdown preview with proper ESC clearing
  6. // @author Johan
  7. // @match *://cr8r.gg/*
  8. // @match *://tilde.zone/*
  9. // @match *://infosec.exchange/*
  10. // @match *://toot.cat/*
  11. // @grant none
  12. // ==/UserScript==
  13.  
  14. (function() {
  15. 'use strict';
  16.  
  17. // 1. Inline-разметка
  18. function inlineMarkdown(text) {
  19. return text
  20. // Ссылки [текст](URL) — улучшенный регексп
  21. .replace(/\[([^\]]+)\]\((https?:\/\/[^\s)]+)\)/g, '<a href="$2" target="_blank" rel="noopener nofollow noreferrer" class="status-link unhandled-link">$1</a>')
  22. // .replace(/#([а-яa-z0-9_]+)/gi, '<span href="" class="mention hashtag status-link">#$1</span>')
  23. .replace( /#([а-яa-z0-9_]+)/gi, (_, tag) => '<a href="' + window.location.origin + '/tags/' + tag + '" class="mention hashtag status-link">#' + tag + '</a>') // теги
  24. .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>')
  25. .replace(/\[(.*?)\]\((.*?)\)/g, '<a href="$2" target="_blank">$1</a>')
  26. .replace(/`(.*?)`/g, '<code>$1</code>')
  27. .replace(/~~(.*?)~~/g, '<del>$1</del>')
  28. .replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
  29. .replace(/__(.*?)__/g, '<strong>$1</strong>')
  30. .replace(/^#{2,6}\s(.+)$/gm, '<strong>$1</strong>')
  31. .replace(/\*(.*?)\*/g, '<em>$1</em>')
  32. .replace(/_(.*?)_/g, '<em>$1</em>')
  33. }
  34.  
  35. // 2. Парсинг Markdown
  36. function parseMarkdown(raw) {
  37. const lines = raw.split('\n');
  38. let result = '';
  39. let inQuote = false;
  40. let inCodeBlock = false;
  41. let codeContent = [];
  42. let currentParagraph = [];
  43. let inList = false;
  44. let listType = '';
  45.  
  46. function flushParagraph() {
  47. if (currentParagraph.length === 0) return;
  48.  
  49. let content = currentParagraph.join('\n').trim();
  50. if (!content) {
  51. currentParagraph = [];
  52. return;
  53. }
  54.  
  55. if (inQuote) {
  56. content = content.replace(/^>\s?/gm, '');
  57. result += `<blockquote><p>${inlineMarkdown(content)}</p></blockquote>`;
  58. } else if (listType) {
  59. const listItems = content.split('\n');
  60. result += `<${listType === 'unordered' ? 'ul' : 'ol'}>`;
  61. listItems.forEach(item => {
  62. const text = item.replace(/^(?:-|\d+\.)\s+/, '').trim();
  63. if (text) result += `<li>${inlineMarkdown(text)}</li>`;
  64. });
  65. result += `</${listType === 'unordered' ? 'ul' : 'ol'}>`;
  66. } else {
  67. result += `<p>${inlineMarkdown(content)}</p>`;
  68. }
  69.  
  70. currentParagraph = [];
  71. }
  72.  
  73. function flushCodeBlock() {
  74. if (codeContent.length === 0) return;
  75. result += `<pre><code>${codeContent.join('\n')
  76. .replace(/&/g, '&amp;')
  77. .replace(/</g, '&lt;')
  78. .replace(/>/g, '&gt;')
  79. }</code></pre>`;
  80. codeContent = [];
  81. }
  82.  
  83. for (const line of lines) {
  84. const trimmedLine = line.trim();
  85.  
  86. if (trimmedLine.startsWith('```')) {
  87. if (inCodeBlock) {
  88. flushCodeBlock();
  89. inCodeBlock = false;
  90. } else {
  91. flushParagraph();
  92. inCodeBlock = true;
  93. }
  94. continue;
  95. }
  96.  
  97. if (inCodeBlock) {
  98. codeContent.push(line);
  99. continue;
  100. }
  101.  
  102. if (trimmedLine.startsWith('# ')) {
  103. flushParagraph();
  104. result += `<h1>${trimmedLine.substring(2)}</h1>`;
  105. continue;
  106. }
  107.  
  108. const isQuoteLine = line.startsWith('>');
  109. if (isQuoteLine && !inQuote) {
  110. flushParagraph();
  111. inQuote = true;
  112. } else if (!isQuoteLine && inQuote) {
  113. flushParagraph();
  114. inQuote = false;
  115. }
  116.  
  117. const isListLine = /^(-|\d+\.)\s+/.test(trimmedLine);
  118. if (isListLine && !inList) {
  119. flushParagraph();
  120. inList = true;
  121. listType = trimmedLine.startsWith('-') ? 'unordered' : 'ordered';
  122. } else if (!isListLine && inList) {
  123. flushParagraph();
  124. inList = false;
  125. listType = '';
  126. }
  127.  
  128. if (trimmedLine === '') {
  129. flushParagraph();
  130. continue;
  131. }
  132.  
  133. currentParagraph.push(line);
  134. }
  135.  
  136. flushParagraph();
  137. flushCodeBlock();
  138. return result;
  139. }
  140.  
  141. function setupMarkdownPreview(textarea) {
  142. textarea.style.height = '200px'; // -------------- Здесь высота окна ввода
  143. textarea.style.minHeight = '200px'; //--------|
  144. textarea.style.maxHeight = '200px'; //--------|
  145. let preview = textarea.nextElementSibling;
  146.  
  147. // Если превью ещё не создано — создаём
  148. if (!preview || !preview.classList.contains('md-preview')) {
  149. preview = document.createElement('div');
  150. preview.className = 'md-preview status__content__text status__content__text--visible translate';
  151. preview.style.cssText = `
  152. border-top: 1px solid #ccc;
  153. padding: 10px;
  154. margin-top: 5px;
  155. white-space: normal;
  156. overflow-y: auto;
  157. overflow-x: auto;
  158. height:440px; //--------------------------- Здесь высота окна просмотра
  159. overflow-wrap: break-word;
  160. `;
  161. textarea.parentNode.insertBefore(preview, textarea.nextSibling);
  162. }
  163.  
  164. // Слушатель ввода: обновляет превью
  165. textarea.addEventListener('input', () => {
  166. handleInputEvent(preview);
  167. preview.innerHTML = parseMarkdown(textarea.value);
  168. maybeScrollToBottom(preview);
  169. });
  170.  
  171. // Слушатель клавиш: очищает превью при Esc
  172. textarea.addEventListener('keydown', (e) => {
  173. if (e.key === 'Escape' && textarea.value === '') {
  174. handleInputEvent(preview);
  175. preview.innerHTML = '';
  176. maybeScrollToBottom(preview);
  177. // e.preventDefault(); // если нужно — предотврати дефолтное поведение
  178. }
  179. });
  180.  
  181. // Начальная отрисовка
  182. preview.innerHTML = parseMarkdown(textarea.value);
  183. }
  184. function init() {
  185. const textarea = document.querySelector('textarea.autosuggest-textarea__textarea');
  186. if (textarea) {
  187. setupMarkdownPreview(textarea);
  188. }
  189. }
  190.  
  191.  
  192. /*
  193. // Собственные стили, пока не работает
  194. const style = document.createElement('style');
  195. style.textContent = `
  196. .md-preview {
  197. font-family: inherit;
  198. color: inherit;
  199. }
  200. .md-preview blockquote {
  201. border-left: 3px solid #4a90e2 !mportant;
  202. padding-left: 1em !mportant;
  203. margin: 0.8em 0 !mportant;
  204. color: #333 !mportant;
  205. }
  206. .md-preview p {
  207. margin: 0.5em 0 !mportant;
  208. line-height: 1. !mportant;
  209. }
  210. .md-preview code {
  211. background: #f0f0f0 !mportant;
  212. padding: 0.2em 0.4em !mportant;
  213. border-radius: 3px !mportant;
  214. font-family: monospace !mportant;
  215. }
  216. .md-preview pre {
  217. background: #f5f5f5 !mportant;
  218. padding: 1em !mportant;
  219. overflow: auto !mportant;
  220. border-radius: 3px !mportant;
  221. }
  222. .md-preview h1 {
  223. font-size: 1.2em !mportant;
  224. margin: 1em 0 0.5em 0 !mportant;
  225. font-weight: bold !mportant;
  226. }
  227. .md-preview ul, .md-preview ol {
  228. margin: 0.5em 0 !mportant;
  229. padding-left: 2em !mportant;
  230. }
  231. `;
  232. document.head.appendChild(style);
  233. */
  234. // Запуск
  235. if (document.readyState === 'complete') {
  236. init();
  237. } else {
  238. window.addEventListener('load', init);
  239. }
  240.  
  241. // Проверка, находится ли scroll внизу (или почти внизу)
  242. function isScrolledToBottom(el) {
  243. return el.scrollHeight - el.scrollTop <= el.clientHeight + 1;
  244. }
  245.  
  246. // Функция, которая запоминает, был ли scroll внизу
  247. let wasAtBottom = true; // по умолчанию считаем, что внизу
  248.  
  249. function handleInputEvent(viewer) {
  250. wasAtBottom = isScrolledToBottom(viewer);
  251. }
  252.  
  253. // Функция, которую надо вызывать после обновления контента
  254. function maybeScrollToBottom(viewer) {
  255. if (wasAtBottom) {
  256. viewer.scrollTop = viewer.scrollHeight;
  257. }
  258. }
  259.  
  260. })();
  261.  
  262.  
Advertisement
Add Comment
Please, Sign In to add comment