Changeset 3480837
- Timestamp:
- 03/12/2026 06:24:58 AM (3 weeks ago)
- Location:
- markdown-renderer-for-github/trunk
- Files:
-
- 9 edited
-
assets/js/gfmr-diagrams.js (modified) (8 diffs)
-
assets/js/gfmr-main.js (modified) (1 diff)
-
assets/js/gfmr-multilingual.js (modified) (1 diff)
-
assets/js/gfmr-plantuml-handler.js (modified) (3 diffs)
-
assets/js/gfmr-ssr-client.js (modified) (2 diffs)
-
changelog.txt (modified) (2 diffs)
-
includes/class-gfmr-language-switcher.php (modified) (1 diff)
-
markdown-renderer-for-github.php (modified) (1 diff)
-
readme.txt (modified) (4 diffs)
Legend:
- Unmodified
- Added
- Removed
-
markdown-renderer-for-github/trunk/assets/js/gfmr-diagrams.js
r3447454 r3480837 384 384 385 385 if (renderResult && renderResult.svg) { 386 // Apply SVG result 386 // Apply SVG result (zoom setup deferred to inside rAF to fix race condition) 387 387 this.applySvgResult(element, renderResult.svg, diagramId, options); 388 389 // Add zoom functionality390 if (options.enableZoom || this.utils.getConfig('mermaid.zoom', false)) {391 this.enableZoomForDiagram(element, diagramId);392 }393 388 394 389 return renderResult; … … 534 529 } 535 530 531 // Fit SVG to container width if it overflows (prevents horizontal scroll) 532 // Zoom setup is deferred into the rAF callback to fix race condition on gfmrFitScale 533 const svgEl = wrapper.querySelector('svg'); 534 const enableZoom = options.enableZoom || this.utils.getConfig('mermaid.zoom', false); 535 if (svgEl) { 536 // preWidth: pass already-measured width to avoid re-measuring (H2) 537 const doFit = (preWidth) => { 538 const containerWidth = 539 preWidth || wrapper.parentElement?.offsetWidth || wrapper.offsetWidth; 540 if (!containerWidth) return; 541 // Prefer viewBox to avoid double-scaling when getBoundingClientRect returns scaled values 542 const viewBox = svgEl.viewBox?.baseVal; 543 let svgNaturalWidth, svgNaturalHeight; 544 if (viewBox && viewBox.width && viewBox.height) { 545 svgNaturalWidth = viewBox.width; 546 svgNaturalHeight = viewBox.height; 547 // Store for applyZoom usage (M3) 548 if (!svgEl.dataset.gfmrNaturalWidth) { 549 svgEl.dataset.gfmrNaturalWidth = svgNaturalWidth; 550 svgEl.dataset.gfmrNaturalHeight = svgNaturalHeight; 551 } 552 } else if (svgEl.dataset.gfmrNaturalWidth) { 553 // Use stored values on re-calls to avoid double-scaling 554 svgNaturalWidth = parseFloat(svgEl.dataset.gfmrNaturalWidth); 555 svgNaturalHeight = parseFloat(svgEl.dataset.gfmrNaturalHeight); 556 } else { 557 const rect = svgEl.getBoundingClientRect(); // H1: single call 558 svgNaturalWidth = rect.width; 559 svgNaturalHeight = rect.height; 560 svgEl.dataset.gfmrNaturalWidth = svgNaturalWidth; 561 svgEl.dataset.gfmrNaturalHeight = svgNaturalHeight; 562 } 563 if (svgNaturalWidth && svgNaturalWidth > containerWidth) { 564 const scale = containerWidth / svgNaturalWidth; 565 svgEl.style.transform = `scale(${scale})`; 566 svgEl.style.transformOrigin = 'top left'; 567 // Left-align SVG so transform-origin matches visual origin 568 svgEl.style.setProperty('margin', '0', 'important'); 569 wrapper.style.height = `${svgNaturalHeight * scale}px`; 570 // Use setProperty to override CSS !important rules (M1) 571 wrapper.style.setProperty('overflow', 'hidden', 'important'); 572 svgEl.dataset.gfmrFitScale = scale; 573 svgEl.dataset.gfmrNaturalHeight = svgNaturalHeight; 574 } 575 // Setup zoom after fit scale is stored (fixes rAF race condition) 576 if (enableZoom) { 577 this.enableZoomForDiagram(wrapper, diagramId); 578 } 579 }; 580 requestAnimationFrame(() => { 581 const containerWidth = 582 wrapper.parentElement?.offsetWidth || wrapper.offsetWidth; 583 if (!containerWidth) { 584 // Container not visible yet - retry when it becomes visible 585 const resizeObserver = new ResizeObserver((entries) => { 586 for (const entry of entries) { 587 if (entry.contentRect.width > 0) { 588 resizeObserver.disconnect(); 589 doFit(); // fresh measurement after becoming visible 590 } 591 } 592 }); 593 resizeObserver.observe(wrapper); 594 } else { 595 doFit(containerWidth); // H2: reuse already-measured width 596 } 597 }); 598 } else if (enableZoom) { 599 // No SVG yet but zoom was requested - set up controls without fitting 600 requestAnimationFrame(() => this.enableZoomForDiagram(wrapper, diagramId)); 601 } 602 536 603 this.utils.debug(`Diagram ${diagramId} rendered successfully`); 537 604 } … … 596 663 enableZoomForDiagram(wrapper, diagramId) { 597 664 if (!wrapper || this.zoomConfig.enabled === false) { 665 return; 666 } 667 668 // Guard against duplicate setup (M2: ResizeObserver may fire doFit multiple times) 669 if (wrapper.querySelector('.gfmr-mermaid-zoom-controls')) { 598 670 return; 599 671 } … … 640 712 controls.querySelector('.zoom-in').addEventListener('click', () => { 641 713 currentZoom = Math.min(currentZoom * 1.2, this.zoomConfig.maxZoom); 642 this.applyZoom(svg, controls, currentZoom );714 this.applyZoom(svg, controls, currentZoom, wrapper); // M4: pass cached wrapper 643 715 }); 644 716 … … 646 718 controls.querySelector('.zoom-out').addEventListener('click', () => { 647 719 currentZoom = Math.max(currentZoom / 1.2, this.zoomConfig.minZoom); 648 this.applyZoom(svg, controls, currentZoom );720 this.applyZoom(svg, controls, currentZoom, wrapper); // M4: pass cached wrapper 649 721 }); 650 722 … … 652 724 controls.querySelector('.zoom-reset').addEventListener('click', () => { 653 725 currentZoom = 1.0; 654 this.applyZoom(svg, controls, currentZoom );726 this.applyZoom(svg, controls, currentZoom, wrapper); // M4: pass cached wrapper 655 727 }); 656 728 … … 661 733 currentZoom = Math.max(this.zoomConfig.minZoom, 662 734 Math.min(this.zoomConfig.maxZoom, currentZoom * delta)); 663 this.applyZoom(svg, controls, currentZoom );735 this.applyZoom(svg, controls, currentZoom, wrapper); // M4: pass cached wrapper 664 736 }); 665 737 } … … 667 739 /** 668 740 * Apply zoom 669 */ 670 applyZoom(svg, controls, zoom) { 671 svg.style.transform = `scale(${zoom})`; 672 svg.style.transformOrigin = 'center center'; 741 * @param {SVGElement} svg 742 * @param {Element} controls 743 * @param {number} zoom 744 * @param {Element} [cachedWrapper] - pre-resolved wrapper to avoid DOM traversal on hot path (M4) 745 */ 746 applyZoom(svg, controls, zoom, cachedWrapper) { 747 // Compose with fit scale so zoom is relative to the fitted size 748 const fitScale = parseFloat(svg.dataset.gfmrFitScale) || 1.0; 749 const totalScale = fitScale * zoom; 750 svg.style.transform = `scale(${totalScale})`; 751 svg.style.transformOrigin = 'top left'; 752 753 // Update wrapper height so zoomed content doesn't overflow or get clipped 754 const naturalHeight = parseFloat(svg.dataset.gfmrNaturalHeight); 755 if (naturalHeight) { 756 // M4: use cachedWrapper from closure to avoid closest() on every zoom event 757 const wrapperEl = cachedWrapper || svg.closest('.gfmr-mermaid-wrapper'); 758 if (wrapperEl) { 759 wrapperEl.style.height = `${naturalHeight * totalScale}px`; 760 } 761 } 673 762 674 763 const zoomPercent = Math.round(zoom * 100); -
markdown-renderer-for-github/trunk/assets/js/gfmr-main.js
r3478700 r3480837 1416 1416 // Keep SVG at natural size (removing max-width prevents text clipping) 1417 1417 svgElement.style.height = "auto"; 1418 1419 // Fit SVG to container width if it overflows (prevents horizontal scroll) 1420 // preWidth: pass already-measured width to avoid re-measuring (H2) 1421 const doFit = (preWidth) => { 1422 const width = 1423 preWidth || outerContainer.parentElement?.offsetWidth || 1424 outerContainer.offsetWidth; 1425 if (!width) return; 1426 // Prefer viewBox to avoid double-scaling when getBoundingClientRect returns scaled values 1427 const viewBox = svgElement.viewBox?.baseVal; 1428 let svgNaturalWidth, svgNaturalHeight; 1429 if (viewBox && viewBox.width && viewBox.height) { 1430 svgNaturalWidth = viewBox.width; 1431 svgNaturalHeight = viewBox.height; 1432 // Store for applyZoom usage and re-call protection (M3) 1433 if (!svgElement.dataset.gfmrNaturalWidth) { 1434 svgElement.dataset.gfmrNaturalWidth = svgNaturalWidth; 1435 svgElement.dataset.gfmrNaturalHeight = svgNaturalHeight; 1436 } 1437 } else if (svgElement.dataset.gfmrNaturalWidth) { 1438 // Use stored values on re-calls to avoid double-scaling 1439 svgNaturalWidth = parseFloat(svgElement.dataset.gfmrNaturalWidth); 1440 svgNaturalHeight = parseFloat(svgElement.dataset.gfmrNaturalHeight); 1441 } else { 1442 const rect = svgElement.getBoundingClientRect(); // H1: single call 1443 svgNaturalWidth = rect.width; 1444 svgNaturalHeight = rect.height; 1445 svgElement.dataset.gfmrNaturalWidth = svgNaturalWidth; 1446 svgElement.dataset.gfmrNaturalHeight = svgNaturalHeight; 1447 } 1448 if (svgNaturalWidth && svgNaturalWidth > width) { 1449 const scale = width / svgNaturalWidth; 1450 svgElement.style.transform = `scale(${scale})`; 1451 svgElement.style.transformOrigin = "top left"; 1452 innerContainer.style.height = `${svgNaturalHeight * scale}px`; 1453 // Use setProperty to override CSS !important rules 1454 outerContainer.style.setProperty("justify-content", "flex-start", "important"); 1455 outerContainer.style.setProperty("overflow", "hidden", "important"); 1456 svgElement.dataset.gfmrFitScale = scale; 1457 svgElement.dataset.gfmrNaturalHeight = svgNaturalHeight; 1458 } 1459 }; 1460 requestAnimationFrame(() => { 1461 const containerWidth = 1462 outerContainer.parentElement?.offsetWidth || 1463 outerContainer.offsetWidth; 1464 if (!containerWidth) { 1465 // Container not visible yet - retry when it becomes visible 1466 const resizeObserver = new ResizeObserver((entries) => { 1467 for (const entry of entries) { 1468 if (entry.contentRect.width > 0) { 1469 resizeObserver.disconnect(); 1470 doFit(); // fresh measurement after becoming visible 1471 } 1472 } 1473 }); 1474 resizeObserver.observe(outerContainer); 1475 } else { 1476 doFit(containerWidth); // H2: reuse already-measured width 1477 } 1478 }); 1418 1479 } 1419 1480 -
markdown-renderer-for-github/trunk/assets/js/gfmr-multilingual.js
r3480739 r3480837 233 233 if ( this.labelStyle === 'flag' ) { 234 234 btn.className += ' gfmr-lang-btn--flag-only'; 235 // スクリーンリーダー向けに人間が読める言語名を使用(WCAG準拠)235 // Use human-readable language name for screen readers (WCAG compliance) 236 236 btn.setAttribute( 'aria-label', this.langNameMap[ lang ] || lang.toUpperCase() ); 237 237 } -
markdown-renderer-for-github/trunk/assets/js/gfmr-plantuml-handler.js
r3480739 r3480837 158 158 const utf8 = this.encodeUtf8(content); 159 159 160 // pako が利用可能なら DEFLATE 圧縮 + PlantUML base64160 // Use DEFLATE compression + PlantUML base64 if pako is available 161 161 if (global.pako && global.pako.deflateRaw) { 162 162 const compressed = global.pako.deflateRaw(utf8, { level: 9 }); … … 293 293 svgPromise = this.fetchSvg(url, options); 294 294 } 295 // .catch()は同期チェーンのため、フォールバック込みのPromise全体をセット295 // Store the entire promise (including fallback) since .catch() is part of the sync chain 296 296 this.inflightRequests.set(cacheKey, svgPromise); 297 297 } … … 372 372 } 373 373 } catch (e) { 374 // JSON パース失敗(HTMLエラーページなど)はデフォルトメッセージを使用374 // JSON parse failure (e.g. HTML error page) — use default message 375 375 } 376 376 throw new Error(errorMsg); -
markdown-renderer-for-github/trunk/assets/js/gfmr-ssr-client.js
r3435320 r3480837 127 127 128 128 /** 129 * Fit SVG to container width to prevent horizontal scrolling. 130 * Uses transform: scale() so text is never clipped. 131 * 132 * @param {Element} container - The .gfmr-mermaid-container element 133 */ 134 function fitSvgToContainer(container) { 135 const svgEl = container.querySelector('svg'); 136 if (!svgEl) return; 137 138 // preWidth: pass already-measured width to avoid re-measuring (H2) 139 const doFit = (preWidth) => { 140 const containerWidth = 141 preWidth || container.parentElement?.offsetWidth || container.offsetWidth; 142 if (!containerWidth) return; 143 // Prefer viewBox to avoid double-scaling when getBoundingClientRect returns scaled values 144 const viewBox = svgEl.viewBox?.baseVal; 145 let svgNaturalWidth, svgNaturalHeight; 146 if (viewBox && viewBox.width && viewBox.height) { 147 svgNaturalWidth = viewBox.width; 148 svgNaturalHeight = viewBox.height; 149 // Store for applyZoom usage and re-call protection (M3) 150 if (!svgEl.dataset.gfmrNaturalWidth) { 151 svgEl.dataset.gfmrNaturalWidth = svgNaturalWidth; 152 svgEl.dataset.gfmrNaturalHeight = svgNaturalHeight; 153 } 154 } else if (svgEl.dataset.gfmrNaturalWidth) { 155 // Use stored values on re-calls to avoid double-scaling 156 svgNaturalWidth = parseFloat(svgEl.dataset.gfmrNaturalWidth); 157 svgNaturalHeight = parseFloat(svgEl.dataset.gfmrNaturalHeight); 158 } else { 159 const rect = svgEl.getBoundingClientRect(); // H1: single call 160 svgNaturalWidth = rect.width; 161 svgNaturalHeight = rect.height; 162 svgEl.dataset.gfmrNaturalWidth = svgNaturalWidth; 163 svgEl.dataset.gfmrNaturalHeight = svgNaturalHeight; 164 } 165 if (svgNaturalWidth && svgNaturalWidth > containerWidth) { 166 const scale = containerWidth / svgNaturalWidth; 167 svgEl.style.transform = `scale(${scale})`; 168 svgEl.style.transformOrigin = 'top left'; 169 // innerContainer may wrap the SVG — find it or use container itself 170 const inner = container.querySelector('.gfmr-mermaid-inner-container') || container; 171 inner.style.height = `${svgNaturalHeight * scale}px`; 172 // Use setProperty to override CSS !important rules on gfmr-mermaid-container 173 container.style.setProperty('justify-content', 'flex-start', 'important'); 174 container.style.setProperty('overflow', 'hidden', 'important'); 175 svgEl.dataset.gfmrFitScale = scale; 176 svgEl.dataset.gfmrNaturalHeight = svgNaturalHeight; 177 } 178 }; 179 180 requestAnimationFrame(() => { 181 const containerWidth = 182 container.parentElement?.offsetWidth || container.offsetWidth; 183 if (!containerWidth) { 184 // Container not visible yet - retry when it becomes visible 185 const resizeObserver = new ResizeObserver((entries) => { 186 for (const entry of entries) { 187 if (entry.contentRect.width > 0) { 188 resizeObserver.disconnect(); 189 doFit(); // fresh measurement after becoming visible 190 } 191 } 192 }); 193 resizeObserver.observe(container); 194 } else { 195 doFit(containerWidth); // H2: reuse already-measured width 196 } 197 }); 198 } 199 200 /** 201 * Apply fitting to all SSR-cached diagrams on the page. 202 */ 203 function fitSsrDiagrams() { 204 const ssrDiagrams = document.querySelectorAll('.gfmr-mermaid-container[data-ssr="true"]'); 205 ssrDiagrams.forEach(fitSvgToContainer); 206 } 207 208 /** 129 209 * Initialize SSR client 130 210 */ … … 137 217 138 218 console.log('[GFMR SSR] Initializing SSR client'); 219 220 // Apply fit to already-cached SSR diagrams (served as static HTML) 221 fitSsrDiagrams(); 139 222 140 223 // Process SSR-pending diagrams -
markdown-renderer-for-github/trunk/changelog.txt
r3480772 r3480837 7 7 8 8 ## [Unreleased] 9 10 ## [2.7.4] - 2026-03-12 11 ### Fixed 12 - fix SVG fit-to-container and zoom quality issues 9 13 10 14 ## [2.7.3] - 2026-03-12 … … 92 96 - Implement Chart.js integration for diagram rendering 93 97 ### Fixed 94 - Mermaid図の文字見切れを修正(useMaxWidth統一・CSSスクロール責務改善)98 - Fix Mermaid diagram text clipping (unify useMaxWidth and improve CSS scroll responsibility) 95 99 - Unify theme settings retrieval between editor and frontend 96 100 - Convert remaining Japanese entries to English in changelog.txt -
markdown-renderer-for-github/trunk/includes/class-gfmr-language-switcher.php
r3480739 r3480837 240 240 } 241 241 242 // Dropdown では flag のみ表示はブラウザ依存の問題が大きいため flag_text にフォールバック242 // In dropdown mode, flag-only display has cross-browser issues, so fall back to flag_text 243 243 if ( 'dropdown' === $display_mode && 'flag' === $label_style ) { 244 244 $label_style = 'flag_text'; -
markdown-renderer-for-github/trunk/markdown-renderer-for-github.php
r3480772 r3480837 4 4 * Plugin URI: https://github.com/wakalab/markdown-renderer-for-github 5 5 * Description: Renders GFM (GitHub Flavored Markdown) content beautifully on the front end using JavaScript libraries. It supports syntax highlighting for code blocks and diagram rendering with Mermaid.js. 6 * Version: 2.7. 36 * Version: 2.7.4 7 7 * Requires at least: 6.5 8 8 * Requires PHP: 8.1 -
markdown-renderer-for-github/trunk/readme.txt
r3480772 r3480837 6 6 Tested up to: 6.9.1 7 7 Requires PHP: 8.1 8 Stable tag: 2.7. 38 Stable tag: 2.7.4 9 9 License: GPLv2 or later 10 10 License URI: https://www.gnu.org/licenses/gpl-2.0.html … … 142 142 143 143 == Changelog == 144 145 = 2.7.4 = 146 * fix SVG fit-to-container and zoom quality issues 144 147 145 148 = 2.7.3 = … … 206 209 * Filter out developer-only commits from changelog 207 210 * Implement Chart.js integration for diagram rendering 208 * Mermaid図の文字見切れを修正(useMaxWidth統一・CSSスクロール責務改善)211 * Fix Mermaid diagram text clipping (unify useMaxWidth and improve CSS scroll responsibility) 209 212 * Unify theme settings retrieval between editor and frontend 210 213 * Convert remaining Japanese entries to English in changelog.txt … … 298 301 * Add coverage-js to gitignore 299 302 300 = 1.11.1 =301 * Fix Rust syntax highlighting across editor and language detection302 303 303 == Upgrade Notice == 304 304
Note: See TracChangeset
for help on using the changeset viewer.