Cursor Rule Markdown Renderer for GitHub

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

La data de 28-05-2025. Vezi ultima versiune.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

  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. })();