您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Renders Cursor Rules (*.mdc) markdown on GitHub into actual Markdown locally, using the marked library + highlight.js.
- // ==UserScript==
- // @name Cursor Rule Markdown Renderer for GitHub
- // @namespace https://github.com/texarkanine
- // @version 1.5.0
- // @description Renders Cursor Rules (*.mdc) markdown on GitHub into actual Markdown locally, using the marked library + highlight.js.
- // @author Texarkanine
- // @licence GPLv3
- // @homepageURL https://github.com/texarkanine/client-side-mdc-render
- // @supportURL https://github.com/texarkanine/client-side-mdc-render/issues
- // @match https://github.com/*
- // @icon data:image/png;base64,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
- // @grant GM_addStyle
- // @require https://cdn.jsdelivr.net/npm/marked@15/lib/marked.umd.min.js
- // @require https://cdn.jsdelivr.net/npm/marked-footnote@1/dist/index.umd.min.js
- // @require https://cdn.jsdelivr.net/npm/@highlightjs/cdn-assets@11/highlight.min.js
- // ==/UserScript==
- (function() {
- 'use strict';
- const DEBUG = true;
- // Updated regex to match .mdc files
- const MDC_FILE_REGEX = /^https:\/\/github\.com\/.*\.mdc(#.*)?$/;
- const YAML_FRONTMATTER_REGEX = /^---\s*\n([\s\S]*?)\n---\s*\n([\s\S]*)$/;
- // Regex to detect specific anchor types
- const LINE_NUMBER_ANCHOR_REGEX = /^#L\d+$/;
- const FOOTNOTE_ANCHOR_REGEX = /^#footnote-/;
- const RENDERED_LABEL = 'M⇩';
- const SOURCE_LABEL = '.mdc';
- const MAX_RENDER_ATTEMPTS = 50;
- const RENDER_RETRY_INTERVAL = 100;
- const RENDERED_ID = 'client-side-mdc-markdown';
- let currentUrl = location.href;
- let isActive = false;
- let textareaObserver = null;
- GM_addStyle(`
- #client-side-mdc-markdown {
- box-sizing: border-box;
- min-width: 200px;
- max-width: 980px;
- margin: 24px auto 0;
- padding: 45px;
- word-wrap: break-word;
- }
- `);
- /**
- * Gets the current anchor from the URL if present
- * @returns {string|null} The anchor part of the URL, or null if no anchor
- */
- function getCurrentAnchor() {
- const hashIndex = location.href.indexOf('#');
- return hashIndex !== -1 ? location.href.substring(hashIndex) : null;
- }
- /**
- * Determines the default view mode based on the current anchor
- * @returns {'rendered'|'source'} The view mode to use
- */
- function getDefaultViewMode() {
- const anchor = getCurrentAnchor();
- if (!anchor) {
- return 'rendered';
- }
- // If it's a line number anchor, default to source mode
- if (LINE_NUMBER_ANCHOR_REGEX.test(anchor)) {
- return 'source';
- }
- // If it's a footnote anchor, default to rendered mode
- if (FOOTNOTE_ANCHOR_REGEX.test(anchor)) {
- return 'rendered';
- }
- // Default to rendered for all other anchors
- return 'rendered';
- }
- /**
- * Scrolls to the current anchor if it exists
- * @param {string} mode - Current view mode ('rendered' or 'source')
- */
- function scrollToAnchor(mode) {
- const anchor = getCurrentAnchor();
- if (!anchor) return;
- // For line number anchors in source mode, GitHub's native handling works
- if (mode === 'source' && LINE_NUMBER_ANCHOR_REGEX.test(anchor)) {
- return;
- }
- // For footnote anchors in rendered mode, we need to handle scrolling
- if (mode === 'rendered' && FOOTNOTE_ANCHOR_REGEX.test(anchor)) {
- // Use setTimeout to ensure the DOM has updated
- setTimeout(() => {
- const targetElement = document.querySelector(anchor);
- if (targetElement) {
- targetElement.scrollIntoView();
- }
- }, 100);
- }
- }
- /**
- * Processes MDC content by extracting YAML frontmatter and converting it to a code block
- * @param {string} content - Raw MDC file content
- * @returns {string} Processed content with YAML frontmatter as code block
- */
- function processContent(content) {
- const match = content.match(YAML_FRONTMATTER_REGEX);
- if (match) {
- const [, yamlContent, markdownContent] = match;
- return `\`\`\`yaml\n${yamlContent}\n\`\`\`\n\n${markdownContent}`;
- }
- return content;
- }
- /**
- * Creates a button element with GitHub's styling
- * @param {string} label - Button text content
- * @param {string} mode - View mode ('rendered' or 'source')
- * @param {boolean} isSelected - Whether button should be in selected state
- * @returns {HTMLLIElement} Complete button list item element
- */
- function createButton(label, mode, isSelected = false) {
- const li = document.createElement('li');
- li.className = `SegmentedControl-item${isSelected ? ' SegmentedControl-item--selected' : ''}`;
- li.setAttribute('role', 'listitem');
- const button = document.createElement('button');
- button.setAttribute('aria-current', isSelected.toString());
- button.setAttribute('type', 'button');
- button.setAttribute('data-view-component', 'true');
- button.className = 'Button--invisible Button--small Button Button--invisible-noVisuals';
- button.onclick = () => setViewMode(mode);
- const content = document.createElement('span');
- content.className = 'Button-content';
- const labelSpan = document.createElement('span');
- labelSpan.className = 'Button-label';
- labelSpan.setAttribute('data-content', label);
- labelSpan.textContent = label;
- content.appendChild(labelSpan);
- button.appendChild(content);
- li.appendChild(button);
- return li;
- }
- /**
- * Creates a GitHub-styled segmented control for toggling between rendered and source views
- * @param {string} defaultMode - The default view mode to select
- * @returns {HTMLDivElement} Complete toggle button control
- */
- function createToggleButton(defaultMode = 'rendered') {
- const container = document.createElement('div');
- container.className = 'mdc-segmented-control';
- const segmentedControl = document.createElement('segmented-control');
- segmentedControl.setAttribute('data-catalyst', '');
- const ul = document.createElement('ul');
- ul.setAttribute('aria-label', 'MDC view');
- ul.setAttribute('role', 'list');
- ul.setAttribute('data-view-component', 'true');
- ul.className = 'SegmentedControl--small SegmentedControl';
- const isRenderedSelected = defaultMode === 'rendered';
- ul.appendChild(createButton(RENDERED_LABEL, 'rendered', isRenderedSelected));
- ul.appendChild(createButton(SOURCE_LABEL, 'source', !isRenderedSelected));
- segmentedControl.appendChild(ul);
- container.appendChild(segmentedControl);
- return container;
- }
- /**
- * Switches between rendered markdown and source code views
- * @param {'rendered'|'source'} mode - View mode to activate
- */
- function setViewMode(mode) {
- const rendered = document.getElementById(RENDERED_ID);
- const original = document.querySelector('#read-only-cursor-text-area')?.closest('section');
- const buttons = document.querySelectorAll('.mdc-segmented-control .SegmentedControl-item');
- if (!rendered || !original || buttons.length !== 2) return;
- const [renderedItem, sourceItem] = buttons;
- const renderedButton = renderedItem.querySelector('button');
- const sourceButton = sourceItem.querySelector('button');
- const isRenderedMode = mode === 'rendered';
- rendered.style.display = isRenderedMode ? 'block' : 'none';
- original.style.display = isRenderedMode ? 'none' : 'block';
- renderedItem.classList.toggle('SegmentedControl-item--selected', isRenderedMode);
- sourceItem.classList.toggle('SegmentedControl-item--selected', !isRenderedMode);
- if (renderedButton) renderedButton.setAttribute('aria-current', isRenderedMode.toString());
- if (sourceButton) sourceButton.setAttribute('aria-current', (!isRenderedMode).toString());
- // Scroll to anchor if needed
- scrollToAnchor(mode);
- }
- /**
- * Renders MDC content as HTML and inserts it into the page
- * @param {string} defaultMode - The default view mode to select ('rendered' or 'source')
- * @returns {boolean} True if rendering was successful, false otherwise
- */
- function renderMDC(defaultMode = 'rendered') {
- const textarea = document.querySelector('#read-only-cursor-text-area');
- if (!textarea) {
- DEBUG && console.log('[mdc-lite] No textarea found');
- return false;
- }
- const content = textarea.textContent?.trim();
- if (!content) {
- DEBUG && console.log('[mdc-lite] No content in textarea');
- return false;
- }
- const existing = document.getElementById(RENDERED_ID);
- existing?.remove();
- const processedContent = processContent(content);
- const rendered = document.createElement('div');
- rendered.id = RENDERED_ID;
- rendered.className = 'markdown-body';
- rendered.innerHTML = marked.use(markedFootnote()).parse(processedContent);
- rendered.querySelectorAll('pre code').forEach(block => {
- hljs.highlightElement(block);
- });
- const section = textarea.closest('section');
- if (!section?.parentElement) {
- DEBUG && console.log('[mdc-lite] Could not find section to insert rendered content');
- return false;
- }
- section.parentElement.insertBefore(rendered, section);
- // Always show toggle button, but set initial state based on defaultMode
- const toolbar = document.querySelector('.react-blob-header-edit-and-raw-actions');
- if (toolbar && !toolbar.querySelector('.mdc-segmented-control')) {
- toolbar.insertBefore(createToggleButton(defaultMode), toolbar.firstChild);
- }
- // Apply the default view mode
- setViewMode(defaultMode);
- DEBUG && console.log('[mdc-lite] Successfully rendered MDC with mode:', defaultMode);
- return true;
- }
- /**
- * Removes all MDC-related elements and restores original state
- */
- function cleanup() {
- document.getElementById(RENDERED_ID)?.remove();
- document.querySelector('.mdc-segmented-control')?.remove();
- const original = document.querySelector('#read-only-cursor-text-area')?.closest('section');
- if (original) original.style.display = 'block';
- if (textareaObserver) {
- textareaObserver.disconnect();
- textareaObserver = null;
- }
- isActive = false;
- DEBUG && console.log('[mdc-lite] Cleaned up');
- }
- /**
- * Sets up a MutationObserver to watch for textarea content changes and re-render accordingly
- * @returns {boolean} True if observer was successfully set up, false otherwise
- */
- function setupTextareaObserver() {
- const textarea = document.querySelector('#read-only-cursor-text-area');
- if (!textarea) return false;
- textareaObserver?.disconnect();
- textareaObserver = new MutationObserver(() => {
- DEBUG && console.log('[mdc-lite] Textarea content changed, re-rendering');
- renderMDC(getDefaultViewMode());
- });
- textareaObserver.observe(textarea, {
- childList: true,
- subtree: true,
- characterData: true
- });
- DEBUG && console.log('[mdc-lite] Textarea observer set up');
- return true;
- }
- /**
- * Handles page navigation changes, activating or deactivating MDC rendering based on URL
- */
- function handlePageChange() {
- if (MDC_FILE_REGEX.test(location.href)) {
- if (!isActive) {
- DEBUG && console.log('[mdc-lite] MDC file detected:', location.href);
- isActive = true;
- // Determine the default view mode based on anchor
- const defaultMode = getDefaultViewMode();
- DEBUG && console.log('[mdc-lite] Default mode:', defaultMode);
- if (renderMDC(defaultMode)) {
- setupTextareaObserver();
- } else {
- // Content not ready yet - retry with exponential backoff would be better, but keeping simple
- let attempts = 0;
- const interval = setInterval(() => {
- attempts++;
- if (renderMDC(defaultMode)) {
- clearInterval(interval);
- setupTextareaObserver();
- } else if (attempts >= MAX_RENDER_ATTEMPTS) {
- clearInterval(interval);
- DEBUG && console.log('[mdc-lite] Timeout waiting for content');
- }
- }, RENDER_RETRY_INTERVAL);
- }
- } else {
- // SPA navigation to another MDC file - re-render to sync toggle state
- const defaultMode = getDefaultViewMode();
- if (renderMDC(defaultMode)) {
- setupTextareaObserver();
- }
- }
- } else if (isActive) {
- cleanup();
- }
- }
- /**
- * Initializes the userscript by setting up page change detection and handling the current page
- */
- function init() {
- handlePageChange();
- // Monitor for SPA navigation changes
- new MutationObserver(() => {
- if (location.href !== currentUrl) {
- currentUrl = location.href;
- DEBUG && console.log('[mdc-lite] Navigation detected:', currentUrl);
- handlePageChange();
- }
- }).observe(document, { subtree: true, childList: true });
- DEBUG && console.log('[mdc-lite] Initialized');
- }
- if (document.readyState === 'loading') {
- document.addEventListener('DOMContentLoaded', init);
- } else {
- init();
- }
- })();