Cursor Rule Markdown Renderer for GitHub

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

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

  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. // ==/UserScript==
  12.  
  13. (function() {
  14. 'use strict';
  15.  
  16. // Set to true to enable debug logging
  17. const DEBUG = false;
  18.  
  19. // GitHub uses these specific selectors for file content display
  20. const CONTENT_SECTION = 'section';
  21. const MARKDOWN_SOURCE = '#read-only-cursor-text-area';
  22. // Only process .mdc files on GitHub
  23. const MDC_FILE_REGEX = /^https:\/\/github\.com\/.*\.mdc$/;
  24. // Cursor rules have YAML frontmatter that needs special handling
  25. const YAML_FRONTMATTER_REGEX = /^---\s*\n([\s\S]*?)\n---\s*\n([\s\S]*)$/;
  26.  
  27. // GitHub-compatible markdown styles using GitHub's CSS variables for theme compatibility
  28. const MARKDOWN_CSS = `
  29. .markdown-body {
  30. box-sizing: border-box;
  31. min-width: 200px;
  32. max-width: 980px;
  33. margin: 0 auto;
  34. padding: 45px;
  35. background: var(--color-canvas-default, #fff);
  36. color: var(--color-fg-default, #24292f);
  37. font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
  38. font-size: 16px;
  39. line-height: 1.5;
  40. word-wrap: break-word;
  41. }
  42. .markdown-body h1, .markdown-body h2, .markdown-body h3,
  43. .markdown-body h4, .markdown-body h5, .markdown-body h6 {
  44. margin-top: 24px;
  45. margin-bottom: 16px;
  46. font-weight: 600;
  47. line-height: 1.25;
  48. }
  49. .markdown-body p {
  50. margin-top: 0;
  51. margin-bottom: 10px;
  52. }
  53. .markdown-body pre {
  54. background-color: #f6f8fa;
  55. padding: 16px;
  56. overflow: auto;
  57. border-radius: 6px;
  58. }
  59. .markdown-body code {
  60. background-color: #f6f8fa;
  61. padding: 0.2em 0.4em;
  62. border-radius: 6px;
  63. font-size: 85%;
  64. }
  65. .markdown-body blockquote {
  66. padding: 0 1em;
  67. color: #6a737d;
  68. border-left: 0.25em solid #dfe2e5;
  69. }
  70. .markdown-body ul, .markdown-body ol {
  71. padding-left: 2em;
  72. }
  73. .markdown-body table {
  74. border-collapse: collapse;
  75. display: block;
  76. width: 100%;
  77. overflow: auto;
  78. }
  79. .markdown-body th, .markdown-body td {
  80. border: 1px solid #dfe2e5;
  81. padding: 6px 13px;
  82. }
  83. `;
  84.  
  85. let cssInjected = false;
  86.  
  87. /**
  88. * Processes .mdc content by wrapping YAML frontmatter in code blocks.
  89. * This is necessary because cursor rules use YAML frontmatter that should
  90. * be displayed as code rather than parsed as markdown.
  91. */
  92. function processContent(rawContent) {
  93. const match = rawContent.match(YAML_FRONTMATTER_REGEX);
  94. if (match) {
  95. const [, yamlContent, markdownContent] = match;
  96. return `\`\`\`yaml\n${yamlContent}\n\`\`\`\n\n${markdownContent}`;
  97. }
  98. return rawContent;
  99. }
  100.  
  101. /**
  102. * Injects GitHub-compatible markdown styles.
  103. * Uses GitHub's CSS variables to match the current theme automatically.
  104. */
  105. function injectStyles() {
  106. if (cssInjected) return;
  107.  
  108. GM_addStyle(MARKDOWN_CSS);
  109. cssInjected = true;
  110. }
  111.  
  112. /**
  113. * Renders the markdown content by replacing the section content.
  114. * Preserves the original textarea as hidden for potential future reference.
  115. */
  116. function renderMarkdown() {
  117. const section = document.querySelector(CONTENT_SECTION);
  118. const textarea = document.querySelector(MARKDOWN_SOURCE);
  119. if (!section || !textarea) {
  120. return false;
  121. }
  122.  
  123. const rawContent = textarea.textContent;
  124. if (!rawContent) {
  125. return false;
  126. }
  127.  
  128. // Process content and render markdown
  129. const processedContent = processContent(rawContent);
  130. const markdownDiv = document.createElement('div');
  131. markdownDiv.className = 'markdown-body';
  132. markdownDiv.innerHTML = marked.parse(processedContent);
  133.  
  134. // Replace section content while preserving the original textarea
  135. section.innerHTML = '';
  136. textarea.style.display = 'none'; // Keep textarea but hide it
  137. section.appendChild(textarea);
  138. section.appendChild(markdownDiv);
  139.  
  140. injectStyles();
  141. return true;
  142. }
  143.  
  144. let contentObserver = null;
  145. let lastContent = '';
  146.  
  147. /**
  148. * Sets up observation of the textarea content changes.
  149. * This ensures we only render when the actual content changes, not stale content.
  150. */
  151. function observeContentChanges() {
  152. // Clean up any existing observer
  153. if (contentObserver) {
  154. contentObserver.disconnect();
  155. contentObserver = null;
  156. }
  157.  
  158. const textarea = document.querySelector(MARKDOWN_SOURCE);
  159. if (!textarea) {
  160. return false;
  161. }
  162.  
  163. // Check if content is different from last render
  164. const currentContent = textarea.textContent;
  165. if (currentContent && currentContent !== lastContent) {
  166. lastContent = currentContent;
  167. if (renderMarkdown()) {
  168. DEBUG && console.log('[mdc-render] Successfully rendered markdown');
  169. }
  170. }
  171.  
  172. // Set up observer for future content changes
  173. contentObserver = new MutationObserver(() => {
  174. const newContent = textarea.textContent;
  175. if (newContent && newContent !== lastContent) {
  176. lastContent = newContent;
  177. if (renderMarkdown()) {
  178. DEBUG && console.log('[mdc-render] Content changed, re-rendered markdown');
  179. }
  180. }
  181. });
  182.  
  183. // Observe changes to the textarea's text content
  184. contentObserver.observe(textarea, {
  185. childList: true,
  186. subtree: true,
  187. characterData: true
  188. });
  189.  
  190. return true;
  191. }
  192.  
  193. /**
  194. * Waits for the textarea to appear, then sets up content observation.
  195. * GitHub loads content asynchronously, so we need to wait for the textarea.
  196. */
  197. function waitForTextareaAndObserve() {
  198. let attempts = 0;
  199. const maxAttempts = 100; // 10 seconds at 100ms intervals
  200. const checkInterval = setInterval(() => {
  201. attempts++;
  202. if (observeContentChanges()) {
  203. clearInterval(checkInterval);
  204. DEBUG && console.log('[mdc-render] Set up content observation');
  205. } else if (attempts >= maxAttempts) {
  206. clearInterval(checkInterval);
  207. DEBUG && console.log('[mdc-render] Timeout waiting for textarea');
  208. }
  209. }, 100);
  210. }
  211.  
  212. /**
  213. * Handles URL changes to detect navigation to .mdc files.
  214. * GitHub is a SPA, so we need to monitor URL changes via DOM mutations.
  215. */
  216. function handleUrlChange() {
  217. if (MDC_FILE_REGEX.test(location.href)) {
  218. DEBUG && console.log('[mdc-render] MDC file detected:', location.href);
  219. waitForTextareaAndObserve();
  220. }
  221. }
  222.  
  223. // Initialize: handle current page and set up URL change detection
  224. let currentUrl = location.href;
  225. handleUrlChange();
  226.  
  227. // Monitor for URL changes in GitHub's SPA
  228. new MutationObserver(() => {
  229. if (location.href !== currentUrl) {
  230. currentUrl = location.href;
  231. handleUrlChange();
  232. }
  233. }).observe(document, { subtree: true, childList: true });
  234.  
  235. })();