Greasy Fork 还支持 简体中文。

Cursor Rule Markdown Renderer for GitHub

Renders Cursor Rules (*.mdc) markdown on GitHub into actual Markdown locally, using the marked library + highlight.js.

目前為 2025-05-28 提交的版本,檢視 最新版本

  1. // ==UserScript==
  2. // @name Cursor Rule Markdown Renderer for GitHub
  3. // @namespace https://github.com/texarkanine
  4. // @version 2025-05-27
  5. // @description Renders Cursor Rules (*.mdc) markdown on GitHub into actual Markdown locally, using the marked library + highlight.js.
  6. // @author Texarkanine
  7. // @match https://github.com/*
  8. // @icon data:image/png;base64,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
  9. // @grant GM_addStyle
  10. // @require https://cdnjs.cloudflare.com/ajax/libs/marked/15.0.7/marked.min.js
  11. // @require https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/highlight.min.js
  12. // ==/UserScript==
  13.  
  14. (function() {
  15. 'use strict';
  16.  
  17. const DEBUG = true;
  18. const MDC_FILE_REGEX = /^https:\/\/github\.com\/.*\.mdc$/;
  19. const YAML_FRONTMATTER_REGEX = /^---\s*\n([\s\S]*?)\n---\s*\n([\s\S]*)$/;
  20. const RENDERED_LABEL = 'MD🠯';
  21. const SOURCE_LABEL = '.mdc';
  22. const MAX_RENDER_ATTEMPTS = 50;
  23. const RENDER_RETRY_INTERVAL = 100;
  24.  
  25. let currentUrl = location.href;
  26. let isActive = false;
  27. let textareaObserver = null;
  28.  
  29. GM_addStyle(`
  30. .markdown-body {
  31. box-sizing: border-box;
  32. min-width: 200px;
  33. max-width: 980px;
  34. margin: 24px auto 0;
  35. padding: 45px;
  36. word-wrap: break-word;
  37. }
  38. `);
  39.  
  40. /**
  41. * Processes MDC content by extracting YAML frontmatter and converting it to a code block
  42. * @param {string} content - Raw MDC file content
  43. * @returns {string} Processed content with YAML frontmatter as code block
  44. */
  45. function processContent(content) {
  46. const match = content.match(YAML_FRONTMATTER_REGEX);
  47. if (match) {
  48. const [, yamlContent, markdownContent] = match;
  49. return `\`\`\`yaml\n${yamlContent}\n\`\`\`\n\n${markdownContent}`;
  50. }
  51. return content;
  52. }
  53.  
  54. /**
  55. * Creates a button element with GitHub's styling
  56. * @param {string} label - Button text content
  57. * @param {string} mode - View mode ('rendered' or 'source')
  58. * @param {boolean} isSelected - Whether button should be in selected state
  59. * @returns {HTMLLIElement} Complete button list item element
  60. */
  61. function createButton(label, mode, isSelected = false) {
  62. const li = document.createElement('li');
  63. li.className = `SegmentedControl-item${isSelected ? ' SegmentedControl-item--selected' : ''}`;
  64. li.setAttribute('role', 'listitem');
  65.  
  66. const button = document.createElement('button');
  67. button.setAttribute('aria-current', isSelected.toString());
  68. button.setAttribute('type', 'button');
  69. button.setAttribute('data-view-component', 'true');
  70. button.className = 'Button--invisible Button--small Button Button--invisible-noVisuals';
  71. button.onclick = () => setViewMode(mode);
  72.  
  73. const content = document.createElement('span');
  74. content.className = 'Button-content';
  75. const labelSpan = document.createElement('span');
  76. labelSpan.className = 'Button-label';
  77. labelSpan.setAttribute('data-content', label);
  78. labelSpan.textContent = label;
  79.  
  80. content.appendChild(labelSpan);
  81. button.appendChild(content);
  82. li.appendChild(button);
  83.  
  84. return li;
  85. }
  86.  
  87. /**
  88. * Creates a GitHub-styled segmented control for toggling between rendered and source views
  89. * @returns {HTMLDivElement} Complete toggle button control
  90. */
  91. function createToggleButton() {
  92. const container = document.createElement('div');
  93. container.className = 'mdc-segmented-control';
  94.  
  95. const segmentedControl = document.createElement('segmented-control');
  96. segmentedControl.setAttribute('data-catalyst', '');
  97.  
  98. const ul = document.createElement('ul');
  99. ul.setAttribute('aria-label', 'MDC view');
  100. ul.setAttribute('role', 'list');
  101. ul.setAttribute('data-view-component', 'true');
  102. ul.className = 'SegmentedControl--small SegmentedControl';
  103.  
  104. ul.appendChild(createButton(RENDERED_LABEL, 'rendered', true));
  105. ul.appendChild(createButton(SOURCE_LABEL, 'source', false));
  106.  
  107. segmentedControl.appendChild(ul);
  108. container.appendChild(segmentedControl);
  109.  
  110. return container;
  111. }
  112.  
  113. /**
  114. * Switches between rendered markdown and source code views
  115. * @param {'rendered'|'source'} mode - View mode to activate
  116. */
  117. function setViewMode(mode) {
  118. const rendered = document.getElementById('mdc-rendered');
  119. const original = document.querySelector('#read-only-cursor-text-area')?.closest('section');
  120. const buttons = document.querySelectorAll('.mdc-segmented-control .SegmentedControl-item');
  121.  
  122. if (!rendered || !original || buttons.length !== 2) return;
  123.  
  124. const [renderedItem, sourceItem] = buttons;
  125. const renderedButton = renderedItem.querySelector('button');
  126. const sourceButton = sourceItem.querySelector('button');
  127.  
  128. const isRenderedMode = mode === 'rendered';
  129.  
  130. rendered.style.display = isRenderedMode ? 'block' : 'none';
  131. original.style.display = isRenderedMode ? 'none' : 'block';
  132.  
  133. renderedItem.classList.toggle('SegmentedControl-item--selected', isRenderedMode);
  134. sourceItem.classList.toggle('SegmentedControl-item--selected', !isRenderedMode);
  135.  
  136. if (renderedButton) renderedButton.setAttribute('aria-current', isRenderedMode.toString());
  137. if (sourceButton) sourceButton.setAttribute('aria-current', (!isRenderedMode).toString());
  138. }
  139.  
  140. /**
  141. * Renders MDC content as HTML and inserts it into the page
  142. * @returns {boolean} True if rendering was successful, false otherwise
  143. */
  144. function renderMDC() {
  145. const textarea = document.querySelector('#read-only-cursor-text-area');
  146. if (!textarea) {
  147. DEBUG && console.log('[mdc-lite] No textarea found');
  148. return false;
  149. }
  150.  
  151. const content = textarea.textContent?.trim();
  152. if (!content) {
  153. DEBUG && console.log('[mdc-lite] No content in textarea');
  154. return false;
  155. }
  156.  
  157. const existing = document.getElementById('mdc-rendered');
  158. existing?.remove();
  159.  
  160. const processedContent = processContent(content);
  161. const rendered = document.createElement('div');
  162. rendered.id = 'mdc-rendered';
  163. rendered.className = 'markdown-body';
  164. rendered.innerHTML = marked.parse(processedContent);
  165.  
  166. rendered.querySelectorAll('pre code').forEach(block => {
  167. hljs.highlightElement(block);
  168. });
  169.  
  170. const section = textarea.closest('section');
  171. if (!section?.parentElement) {
  172. DEBUG && console.log('[mdc-lite] Could not find section to insert rendered content');
  173. return false;
  174. }
  175.  
  176. section.parentElement.insertBefore(rendered, section);
  177. section.style.display = 'none';
  178.  
  179. const toolbar = document.querySelector('.react-blob-header-edit-and-raw-actions');
  180. if (toolbar && !toolbar.querySelector('.mdc-segmented-control')) {
  181. toolbar.insertBefore(createToggleButton(), toolbar.firstChild);
  182. }
  183.  
  184. // Ensure toggle reflects actual display state (always rendered initially)
  185. setViewMode('rendered');
  186.  
  187. DEBUG && console.log('[mdc-lite] Successfully rendered MDC');
  188. return true;
  189. }
  190.  
  191. /**
  192. * Removes all MDC-related elements and restores original state
  193. */
  194. function cleanup() {
  195. document.getElementById('mdc-rendered')?.remove();
  196. document.querySelector('.mdc-segmented-control')?.remove();
  197.  
  198. const original = document.querySelector('#read-only-cursor-text-area')?.closest('section');
  199. if (original) original.style.display = 'block';
  200.  
  201. if (textareaObserver) {
  202. textareaObserver.disconnect();
  203. textareaObserver = null;
  204. }
  205.  
  206. isActive = false;
  207. DEBUG && console.log('[mdc-lite] Cleaned up');
  208. }
  209.  
  210. /**
  211. * Sets up a MutationObserver to watch for textarea content changes and re-render accordingly
  212. * @returns {boolean} True if observer was successfully set up, false otherwise
  213. */
  214. function setupTextareaObserver() {
  215. const textarea = document.querySelector('#read-only-cursor-text-area');
  216. if (!textarea) return false;
  217.  
  218. textareaObserver?.disconnect();
  219.  
  220. textareaObserver = new MutationObserver(() => {
  221. DEBUG && console.log('[mdc-lite] Textarea content changed, re-rendering');
  222. renderMDC();
  223. });
  224.  
  225. textareaObserver.observe(textarea, {
  226. childList: true,
  227. subtree: true,
  228. characterData: true
  229. });
  230.  
  231. DEBUG && console.log('[mdc-lite] Textarea observer set up');
  232. return true;
  233. }
  234.  
  235. /**
  236. * Handles page navigation changes, activating or deactivating MDC rendering based on URL
  237. */
  238. function handlePageChange() {
  239. if (MDC_FILE_REGEX.test(location.href)) {
  240. if (!isActive) {
  241. DEBUG && console.log('[mdc-lite] MDC file detected:', location.href);
  242. isActive = true;
  243.  
  244. if (renderMDC()) {
  245. setupTextareaObserver();
  246. } else {
  247. // Content not ready yet - retry with exponential backoff would be better, but keeping simple
  248. let attempts = 0;
  249.  
  250. const interval = setInterval(() => {
  251. attempts++;
  252. if (renderMDC()) {
  253. clearInterval(interval);
  254. setupTextareaObserver();
  255. } else if (attempts >= MAX_RENDER_ATTEMPTS) {
  256. clearInterval(interval);
  257. DEBUG && console.log('[mdc-lite] Timeout waiting for content');
  258. }
  259. }, RENDER_RETRY_INTERVAL);
  260. }
  261. } else {
  262. // SPA navigation to another MDC file - re-render to sync toggle state
  263. if (renderMDC()) {
  264. setupTextareaObserver();
  265. }
  266. }
  267. } else if (isActive) {
  268. cleanup();
  269. }
  270. }
  271.  
  272. /**
  273. * Initializes the userscript by setting up page change detection and handling the current page
  274. */
  275. function init() {
  276. handlePageChange();
  277.  
  278. // Monitor for SPA navigation changes
  279. new MutationObserver(() => {
  280. if (location.href !== currentUrl) {
  281. currentUrl = location.href;
  282. DEBUG && console.log('[mdc-lite] Navigation detected:', currentUrl);
  283. handlePageChange();
  284. }
  285. }).observe(document, { subtree: true, childList: true });
  286.  
  287. DEBUG && console.log('[mdc-lite] Initialized');
  288. }
  289.  
  290. if (document.readyState === 'loading') {
  291. document.addEventListener('DOMContentLoaded', init);
  292. } else {
  293. init();
  294. }
  295.  
  296. })();