Cursor Rule Markdown Renderer for GitHub

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

当前为 2025-05-28 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Cursor Rule Markdown Renderer for GitHub
  3. // @namespace http://tampermonkey.net/
  4. // @version 2025-05-27
  5. // @description Renders Cursor Rules (*.mdc) markdown on GitHub into actual Markdown locally, using the marked library.
  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. // Set to true to enable debug logging
  18. const DEBUG = false;
  19.  
  20. // GitHub uses these specific selectors for file content display
  21. const CONTENT_SECTION = 'section';
  22. const MARKDOWN_SOURCE = '#read-only-cursor-text-area';
  23.  
  24. // Only process .mdc files on GitHub
  25. const MDC_FILE_REGEX = /^https:\/\/github\.com\/.*\.mdc$/;
  26.  
  27. // Cursor rules have YAML frontmatter that needs special handling
  28. const YAML_FRONTMATTER_REGEX = /^---\s*\n([\s\S]*?)\n---\s*\n([\s\S]*)$/;
  29.  
  30. // GitHub-compatible markdown styles using GitHub's CSS variables for theme compatibility
  31. const MARKDOWN_CSS = `
  32. .markdown-body {
  33. box-sizing: border-box;
  34. min-width: 200px;
  35. max-width: 980px;
  36. margin: 0 auto;
  37. padding: 45px;
  38. word-wrap: break-word;
  39. }
  40. `;
  41.  
  42. let cssInjected = false;
  43.  
  44. /**
  45. * Processes .mdc content by wrapping YAML frontmatter in code blocks.
  46. * This is necessary because cursor rules use YAML frontmatter that should
  47. * be displayed as code rather than parsed as markdown.
  48. */
  49. function processContent(rawContent) {
  50. const match = rawContent.match(YAML_FRONTMATTER_REGEX);
  51.  
  52. if (match) {
  53. const [, yamlContent, markdownContent] = match;
  54. return `\`\`\`yaml\n${yamlContent}\n\`\`\`\n\n${markdownContent}`;
  55. }
  56.  
  57. return rawContent;
  58. }
  59.  
  60. /**
  61. * Injects GitHub-compatible markdown styles.
  62. * Uses GitHub's CSS variables to match the current theme automatically.
  63. */
  64. function injectStyles() {
  65. if (cssInjected) return;
  66.  
  67. GM_addStyle(MARKDOWN_CSS);
  68. cssInjected = true;
  69. }
  70.  
  71. /**
  72. * Renders the markdown content by replacing the section content.
  73. * Preserves the original textarea as hidden for potential future reference.
  74. */
  75. function renderMarkdown() {
  76. const section = document.querySelector(CONTENT_SECTION);
  77. const textarea = document.querySelector(MARKDOWN_SOURCE);
  78.  
  79. if (!section || !textarea) {
  80. return false;
  81. }
  82.  
  83. const rawContent = textarea.textContent;
  84. if (!rawContent) {
  85. return false;
  86. }
  87.  
  88. // Process content and render markdown
  89. const processedContent = processContent(rawContent);
  90. const markdownDiv = document.createElement('div');
  91. markdownDiv.className = 'markdown-body';
  92. markdownDiv.innerHTML = marked.parse(processedContent);
  93.  
  94. // Syntax highlight code blocks using highlight.js
  95. markdownDiv.querySelectorAll('pre code[class^="language-"]').forEach(block => {
  96. hljs.highlightElement(block);
  97. });
  98. DEBUG && console.log('[mdc-render] highlight.js applied to code blocks');
  99.  
  100. // Replace section content while preserving the original textarea
  101. section.innerHTML = '';
  102. textarea.style.display = 'none'; // Keep textarea but hide it
  103. section.appendChild(textarea);
  104. section.appendChild(markdownDiv);
  105.  
  106. injectStyles();
  107. return true;
  108. }
  109.  
  110. let contentObserver = null;
  111. let lastContent = '';
  112.  
  113. /**
  114. * Sets up observation of the textarea content changes.
  115. * This ensures we only render when the actual content changes, not stale content.
  116. */
  117. function observeContentChanges() {
  118. // Clean up any existing observer
  119. if (contentObserver) {
  120. contentObserver.disconnect();
  121. contentObserver = null;
  122. }
  123.  
  124. const textarea = document.querySelector(MARKDOWN_SOURCE);
  125. if (!textarea) {
  126. return false;
  127. }
  128.  
  129. // Check if content is different from last render
  130. const currentContent = textarea.textContent;
  131. if (currentContent && currentContent !== lastContent) {
  132. lastContent = currentContent;
  133. if (renderMarkdown()) {
  134. DEBUG && console.log('[mdc-render] Successfully rendered markdown');
  135. }
  136. }
  137.  
  138. // Set up observer for future content changes
  139. contentObserver = new MutationObserver(() => {
  140. const newContent = textarea.textContent;
  141. if (newContent && newContent !== lastContent) {
  142. lastContent = newContent;
  143. if (renderMarkdown()) {
  144. DEBUG && console.log('[mdc-render] Content changed, re-rendered markdown');
  145. }
  146. }
  147. });
  148.  
  149. // Observe changes to the textarea's text content
  150. contentObserver.observe(textarea, {
  151. childList: true,
  152. subtree: true,
  153. characterData: true
  154. });
  155.  
  156. return true;
  157. }
  158.  
  159. /**
  160. * Waits for the textarea to appear, then sets up content observation.
  161. * GitHub loads content asynchronously, so we need to wait for the textarea.
  162. */
  163. function waitForTextareaAndObserve() {
  164. let attempts = 0;
  165. const maxAttempts = 100; // 10 seconds at 100ms intervals
  166.  
  167. const checkInterval = setInterval(() => {
  168. attempts++;
  169.  
  170. if (observeContentChanges()) {
  171. clearInterval(checkInterval);
  172. DEBUG && console.log('[mdc-render] Set up content observation');
  173. } else if (attempts >= maxAttempts) {
  174. clearInterval(checkInterval);
  175. DEBUG && console.log('[mdc-render] Timeout waiting for textarea');
  176. }
  177. }, 100);
  178. }
  179.  
  180. /**
  181. * Handles URL changes to detect navigation to .mdc files.
  182. * GitHub is a SPA, so we need to monitor URL changes via DOM mutations.
  183. */
  184. function handleUrlChange() {
  185. if (MDC_FILE_REGEX.test(location.href)) {
  186. DEBUG && console.log('[mdc-render] MDC file detected:', location.href);
  187. waitForTextareaAndObserve();
  188. }
  189. }
  190.  
  191. // Initialize: handle current page and set up URL change detection
  192. let currentUrl = location.href;
  193. handleUrlChange();
  194.  
  195. // Monitor for URL changes in GitHub's SPA
  196. new MutationObserver(() => {
  197. if (location.href !== currentUrl) {
  198. currentUrl = location.href;
  199. handleUrlChange();
  200. }
  201. }).observe(document, { subtree: true, childList: true });
  202.  
  203. })();