mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-31 10:56:10 +01:00 
			
		
		
		
	Fix markdown render behaviors (#34122)
* Fix #27645 * Add config options `MATH_CODE_BLOCK_DETECTION`, problematic syntaxes are disabled by default * Fix #33639 * Add config options `RENDER_OPTIONS_*`, old behaviors are kept
This commit is contained in:
		| @@ -1,16 +1,17 @@ | ||||
| import {queryElems} from '../utils/dom.ts'; | ||||
|  | ||||
| export async function initMarkupRenderAsciicast(elMarkup: HTMLElement): Promise<void> { | ||||
|   const el = elMarkup.querySelector('.asciinema-player-container'); | ||||
|   if (!el) return; | ||||
|   queryElems(elMarkup, '.asciinema-player-container', async (el) => { | ||||
|     const [player] = await Promise.all([ | ||||
|       // @ts-expect-error: module exports no types | ||||
|       import(/* webpackChunkName: "asciinema-player" */'asciinema-player'), | ||||
|       import(/* webpackChunkName: "asciinema-player" */'asciinema-player/dist/bundle/asciinema-player.css'), | ||||
|     ]); | ||||
|  | ||||
|   const [player] = await Promise.all([ | ||||
|     // @ts-expect-error: module exports no types | ||||
|     import(/* webpackChunkName: "asciinema-player" */'asciinema-player'), | ||||
|     import(/* webpackChunkName: "asciinema-player" */'asciinema-player/dist/bundle/asciinema-player.css'), | ||||
|   ]); | ||||
|  | ||||
|   player.create(el.getAttribute('data-asciinema-player-src'), el, { | ||||
|     // poster (a preview frame) to display until the playback is started. | ||||
|     // Set it to 1 hour (also means the end if the video is shorter) to make the preview frame show more. | ||||
|     poster: 'npt:1:0:0', | ||||
|     player.create(el.getAttribute('data-asciinema-player-src'), el, { | ||||
|       // poster (a preview frame) to display until the playback is started. | ||||
|       // Set it to 1 hour (also means the end if the video is shorter) to make the preview frame show more. | ||||
|       poster: 'npt:1:0:0', | ||||
|     }); | ||||
|   }); | ||||
| } | ||||
|   | ||||
| @@ -1,4 +1,5 @@ | ||||
| import {svg} from '../svg.ts'; | ||||
| import {queryElems} from '../utils/dom.ts'; | ||||
|  | ||||
| export function makeCodeCopyButton(): HTMLButtonElement { | ||||
|   const button = document.createElement('button'); | ||||
| @@ -8,11 +9,12 @@ export function makeCodeCopyButton(): HTMLButtonElement { | ||||
| } | ||||
|  | ||||
| export function initMarkupCodeCopy(elMarkup: HTMLElement): void { | ||||
|   const el = elMarkup.querySelector('.code-block code'); // .markup .code-block code | ||||
|   if (!el || !el.textContent) return; | ||||
|  | ||||
|   const btn = makeCodeCopyButton(); | ||||
|   // remove final trailing newline introduced during HTML rendering | ||||
|   btn.setAttribute('data-clipboard-text', el.textContent.replace(/\r?\n$/, '')); | ||||
|   el.after(btn); | ||||
|   // .markup .code-block code | ||||
|   queryElems(elMarkup, '.code-block code', (el) => { | ||||
|     if (!el.textContent) return; | ||||
|     const btn = makeCodeCopyButton(); | ||||
|     // remove final trailing newline introduced during HTML rendering | ||||
|     btn.setAttribute('data-clipboard-text', el.textContent.replace(/\r?\n$/, '')); | ||||
|     el.after(btn); | ||||
|   }); | ||||
| } | ||||
|   | ||||
| @@ -1,4 +1,5 @@ | ||||
| import {displayError} from './common.ts'; | ||||
| import {queryElems} from '../utils/dom.ts'; | ||||
|  | ||||
| function targetElement(el: Element): {target: Element, displayAsBlock: boolean} { | ||||
|   // The target element is either the parent "code block with loading indicator", or itself | ||||
| @@ -12,35 +13,35 @@ function targetElement(el: Element): {target: Element, displayAsBlock: boolean} | ||||
| } | ||||
|  | ||||
| export async function initMarkupCodeMath(elMarkup: HTMLElement): Promise<void> { | ||||
|   const el = elMarkup.querySelector('code.language-math'); // .markup code.language-math' | ||||
|   if (!el) return; | ||||
|   // .markup code.language-math' | ||||
|   queryElems(elMarkup, 'code.language-math', async (el) => { | ||||
|     const [{default: katex}] = await Promise.all([ | ||||
|       import(/* webpackChunkName: "katex" */'katex'), | ||||
|       import(/* webpackChunkName: "katex" */'katex/dist/katex.css'), | ||||
|     ]); | ||||
|  | ||||
|   const [{default: katex}] = await Promise.all([ | ||||
|     import(/* webpackChunkName: "katex" */'katex'), | ||||
|     import(/* webpackChunkName: "katex" */'katex/dist/katex.css'), | ||||
|   ]); | ||||
|     const MAX_CHARS = 1000; | ||||
|     const MAX_SIZE = 25; | ||||
|     const MAX_EXPAND = 1000; | ||||
|  | ||||
|   const MAX_CHARS = 1000; | ||||
|   const MAX_SIZE = 25; | ||||
|   const MAX_EXPAND = 1000; | ||||
|     const {target, displayAsBlock} = targetElement(el); | ||||
|     if (target.hasAttribute('data-render-done')) return; | ||||
|     const source = el.textContent; | ||||
|  | ||||
|   const {target, displayAsBlock} = targetElement(el); | ||||
|   if (target.hasAttribute('data-render-done')) return; | ||||
|   const source = el.textContent; | ||||
|  | ||||
|   if (source.length > MAX_CHARS) { | ||||
|     displayError(target, new Error(`Math source of ${source.length} characters exceeds the maximum allowed length of ${MAX_CHARS}.`)); | ||||
|     return; | ||||
|   } | ||||
|   try { | ||||
|     const tempEl = document.createElement(displayAsBlock ? 'p' : 'span'); | ||||
|     katex.render(source, tempEl, { | ||||
|       maxSize: MAX_SIZE, | ||||
|       maxExpand: MAX_EXPAND, | ||||
|       displayMode: displayAsBlock, // katex: true for display (block) mode, false for inline mode | ||||
|     }); | ||||
|     target.replaceWith(tempEl); | ||||
|   } catch (error) { | ||||
|     displayError(target, error); | ||||
|   } | ||||
|     if (source.length > MAX_CHARS) { | ||||
|       displayError(target, new Error(`Math source of ${source.length} characters exceeds the maximum allowed length of ${MAX_CHARS}.`)); | ||||
|       return; | ||||
|     } | ||||
|     try { | ||||
|       const tempEl = document.createElement(displayAsBlock ? 'p' : 'span'); | ||||
|       katex.render(source, tempEl, { | ||||
|         maxSize: MAX_SIZE, | ||||
|         maxExpand: MAX_EXPAND, | ||||
|         displayMode: displayAsBlock, // katex: true for display (block) mode, false for inline mode | ||||
|       }); | ||||
|       target.replaceWith(tempEl); | ||||
|     } catch (error) { | ||||
|       displayError(target, error); | ||||
|     } | ||||
|   }); | ||||
| } | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| import {isDarkTheme} from '../utils.ts'; | ||||
| import {makeCodeCopyButton} from './codecopy.ts'; | ||||
| import {displayError} from './common.ts'; | ||||
| import {queryElems} from '../utils/dom.ts'; | ||||
|  | ||||
| const {mermaidMaxSourceCharacters} = window.config; | ||||
|  | ||||
| @@ -11,77 +12,77 @@ body {margin: 0; padding: 0; overflow: hidden} | ||||
| blockquote, dd, dl, figure, h1, h2, h3, h4, h5, h6, hr, p, pre {margin: 0}`; | ||||
|  | ||||
| export async function initMarkupCodeMermaid(elMarkup: HTMLElement): Promise<void> { | ||||
|   const el = elMarkup.querySelector('code.language-mermaid'); // .markup code.language-mermaid | ||||
|   if (!el) return; | ||||
|   // .markup code.language-mermaid | ||||
|   queryElems(elMarkup, 'code.language-mermaid', async (el) => { | ||||
|     const {default: mermaid} = await import(/* webpackChunkName: "mermaid" */'mermaid'); | ||||
|  | ||||
|   const {default: mermaid} = await import(/* webpackChunkName: "mermaid" */'mermaid'); | ||||
|  | ||||
|   mermaid.initialize({ | ||||
|     startOnLoad: false, | ||||
|     theme: isDarkTheme() ? 'dark' : 'neutral', | ||||
|     securityLevel: 'strict', | ||||
|     suppressErrorRendering: true, | ||||
|   }); | ||||
|  | ||||
|   const pre = el.closest('pre'); | ||||
|   if (pre.hasAttribute('data-render-done')) return; | ||||
|  | ||||
|   const source = el.textContent; | ||||
|   if (mermaidMaxSourceCharacters >= 0 && source.length > mermaidMaxSourceCharacters) { | ||||
|     displayError(pre, new Error(`Mermaid source of ${source.length} characters exceeds the maximum allowed length of ${mermaidMaxSourceCharacters}.`)); | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   try { | ||||
|     await mermaid.parse(source); | ||||
|   } catch (err) { | ||||
|     displayError(pre, err); | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   try { | ||||
|     // can't use bindFunctions here because we can't cross the iframe boundary. This | ||||
|     // means js-based interactions won't work but they aren't intended to work either | ||||
|     const {svg} = await mermaid.render('mermaid', source); | ||||
|  | ||||
|     const iframe = document.createElement('iframe'); | ||||
|     iframe.classList.add('markup-content-iframe', 'tw-invisible'); | ||||
|     iframe.srcdoc = `<html><head><style>${iframeCss}</style></head><body>${svg}</body></html>`; | ||||
|  | ||||
|     const mermaidBlock = document.createElement('div'); | ||||
|     mermaidBlock.classList.add('mermaid-block', 'is-loading', 'tw-hidden'); | ||||
|     mermaidBlock.append(iframe); | ||||
|  | ||||
|     const btn = makeCodeCopyButton(); | ||||
|     btn.setAttribute('data-clipboard-text', source); | ||||
|     mermaidBlock.append(btn); | ||||
|  | ||||
|     const updateIframeHeight = () => { | ||||
|       const body = iframe.contentWindow?.document?.body; | ||||
|       if (body) { | ||||
|         iframe.style.height = `${body.clientHeight}px`; | ||||
|       } | ||||
|     }; | ||||
|  | ||||
|     iframe.addEventListener('load', () => { | ||||
|       pre.replaceWith(mermaidBlock); | ||||
|       mermaidBlock.classList.remove('tw-hidden'); | ||||
|       updateIframeHeight(); | ||||
|       setTimeout(() => { // avoid flash of iframe background | ||||
|         mermaidBlock.classList.remove('is-loading'); | ||||
|         iframe.classList.remove('tw-invisible'); | ||||
|       }, 0); | ||||
|  | ||||
|       // update height when element's visibility state changes, for example when the diagram is inside | ||||
|       // a <details> + <summary> block and the <details> block becomes visible upon user interaction, it | ||||
|       // would initially set a incorrect height and the correct height is set during this callback. | ||||
|       (new IntersectionObserver(() => { | ||||
|         updateIframeHeight(); | ||||
|       }, {root: document.documentElement})).observe(iframe); | ||||
|     mermaid.initialize({ | ||||
|       startOnLoad: false, | ||||
|       theme: isDarkTheme() ? 'dark' : 'neutral', | ||||
|       securityLevel: 'strict', | ||||
|       suppressErrorRendering: true, | ||||
|     }); | ||||
|  | ||||
|     document.body.append(mermaidBlock); | ||||
|   } catch (err) { | ||||
|     displayError(pre, err); | ||||
|   } | ||||
|     const pre = el.closest('pre'); | ||||
|     if (pre.hasAttribute('data-render-done')) return; | ||||
|  | ||||
|     const source = el.textContent; | ||||
|     if (mermaidMaxSourceCharacters >= 0 && source.length > mermaidMaxSourceCharacters) { | ||||
|       displayError(pre, new Error(`Mermaid source of ${source.length} characters exceeds the maximum allowed length of ${mermaidMaxSourceCharacters}.`)); | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     try { | ||||
|       await mermaid.parse(source); | ||||
|     } catch (err) { | ||||
|       displayError(pre, err); | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     try { | ||||
|       // can't use bindFunctions here because we can't cross the iframe boundary. This | ||||
|       // means js-based interactions won't work but they aren't intended to work either | ||||
|       const {svg} = await mermaid.render('mermaid', source); | ||||
|  | ||||
|       const iframe = document.createElement('iframe'); | ||||
|       iframe.classList.add('markup-content-iframe', 'tw-invisible'); | ||||
|       iframe.srcdoc = `<html><head><style>${iframeCss}</style></head><body>${svg}</body></html>`; | ||||
|  | ||||
|       const mermaidBlock = document.createElement('div'); | ||||
|       mermaidBlock.classList.add('mermaid-block', 'is-loading', 'tw-hidden'); | ||||
|       mermaidBlock.append(iframe); | ||||
|  | ||||
|       const btn = makeCodeCopyButton(); | ||||
|       btn.setAttribute('data-clipboard-text', source); | ||||
|       mermaidBlock.append(btn); | ||||
|  | ||||
|       const updateIframeHeight = () => { | ||||
|         const body = iframe.contentWindow?.document?.body; | ||||
|         if (body) { | ||||
|           iframe.style.height = `${body.clientHeight}px`; | ||||
|         } | ||||
|       }; | ||||
|  | ||||
|       iframe.addEventListener('load', () => { | ||||
|         pre.replaceWith(mermaidBlock); | ||||
|         mermaidBlock.classList.remove('tw-hidden'); | ||||
|         updateIframeHeight(); | ||||
|         setTimeout(() => { // avoid flash of iframe background | ||||
|           mermaidBlock.classList.remove('is-loading'); | ||||
|           iframe.classList.remove('tw-invisible'); | ||||
|         }, 0); | ||||
|  | ||||
|         // update height when element's visibility state changes, for example when the diagram is inside | ||||
|         // a <details> + <summary> block and the <details> block becomes visible upon user interaction, it | ||||
|         // would initially set a incorrect height and the correct height is set during this callback. | ||||
|         (new IntersectionObserver(() => { | ||||
|           updateIframeHeight(); | ||||
|         }, {root: document.documentElement})).observe(iframe); | ||||
|       }); | ||||
|  | ||||
|       document.body.append(mermaidBlock); | ||||
|     } catch (err) { | ||||
|       displayError(pre, err); | ||||
|     } | ||||
|   }); | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user