mirror of
				https://github.com/scm-manager/scm-manager.git
				synced 2025-10-31 18:46:07 +01:00 
			
		
		
		
	add extension point for custom link protocol renderers in markdown (#1639)
This PR allows for custom link protocols to be declared and rendered in markdown.
A new extension point markdown-renderer.link.protocol allows for renderers to hook into the api and implement any custom protocol.
Example:
[description](myprotocol:somelink)
binder.bind("markdown-renderer.link.protocol", { protocol: "myprotocol", renderer: MyProtocolRenderer })
This renderer functions similar to link renderers and receives the href and the description. The latter as the children property.
This PR also fixes two bugs where external- and anchor links were not correctly rendered in pull requests by the review-plugin.
Co-authored-by: Eduard Heimbuch <eduard.heimbuch@cloudogu.com>
			
			
This commit is contained in:
		
				
					committed by
					
						 GitHub
						GitHub
					
				
			
			
				
	
			
			
			
						parent
						
							8f91c217fc
						
					
				
				
					commit
					32b268e6f5
				
			| @@ -62,6 +62,17 @@ The following extension points are provided for the frontend: | |||||||
| - Dynamic extension point for custom language-specific renderers | - Dynamic extension point for custom language-specific renderers | ||||||
| - Overrides the default Syntax Highlighter | - Overrides the default Syntax Highlighter | ||||||
| - Used by the Markdown Plantuml Plugin | - Used by the Markdown Plantuml Plugin | ||||||
|  | ### markdown-renderer.link.protocol | ||||||
|  | - Define custom protocols and their renderers for links in markdown | ||||||
|  |  | ||||||
|  | Example: | ||||||
|  | ```markdown | ||||||
|  | [description](myprotocol:somelink) | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | ```typescript | ||||||
|  | binder.bind("markdown-renderer.link.protocol", { protocol: "myprotocol", renderer: MyProtocolRenderer }) | ||||||
|  | ``` | ||||||
|  |  | ||||||
| # Deprecated | # Deprecated | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										4
									
								
								gradle/changelog/link-renderer.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								gradle/changelog/link-renderer.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,4 @@ | |||||||
|  | - type: added | ||||||
|  |   description: Add extension point for custom link protocol renderers in markdown ([#1639](https://github.com/scm-manager/scm-manager/pull/1639)) | ||||||
|  | - type: fixed | ||||||
|  |   description: External links and anchor links are now correctly rendered in markdown even if no base path is present ([#1639](https://github.com/scm-manager/scm-manager/pull/1639)) | ||||||
| @@ -40,6 +40,10 @@ Anchor Links should be rendered a simple a tag with an href: [anchor link](#samp | |||||||
|  |  | ||||||
| Links with a protocol other than http should be rendered a simple a tag with an href e.g.: [mail link](mailto:marvin@hitchhiker.com) | Links with a protocol other than http should be rendered a simple a tag with an href e.g.: [mail link](mailto:marvin@hitchhiker.com) | ||||||
|  |  | ||||||
|  | ## Custom Protocol | ||||||
|  |  | ||||||
|  | Renderers for custom protocols can be added via the "markdown-renderer.link.protocol" extension point: [description of scw link](scw:marvin@hitchhiker.com) | ||||||
|  |  | ||||||
| ## Internal | ## Internal | ||||||
|  |  | ||||||
| Internal links should be rendered by react-router: [internal link](/buttons) | Internal links should be rendered by react-router: [internal link](/buttons) | ||||||
|   | |||||||
| @@ -47993,6 +47993,8 @@ exports[`Storyshots MarkdownView Commit Links 1`] = ` | |||||||
|       <p> |       <p> | ||||||
|         <a |         <a | ||||||
|           href="https://scm-manager.org/" |           href="https://scm-manager.org/" | ||||||
|  |           rel="noopener noreferrer" | ||||||
|  |           target="_blank" | ||||||
|         > |         > | ||||||
|           hitchhiker/heart-of-gold@42 |           hitchhiker/heart-of-gold@42 | ||||||
|         </a> |         </a> | ||||||
| @@ -48093,7 +48095,7 @@ exports[`Storyshots MarkdownView Default 1`] = ` | |||||||
| 
 | 
 | ||||||
|         <li> |         <li> | ||||||
|           <a |           <a | ||||||
|             href="#Headings" |             href="/#Headings" | ||||||
|           > |           > | ||||||
|             Headings |             Headings | ||||||
|           </a> |           </a> | ||||||
| @@ -48102,7 +48104,7 @@ exports[`Storyshots MarkdownView Default 1`] = ` | |||||||
| 
 | 
 | ||||||
|         <li> |         <li> | ||||||
|           <a |           <a | ||||||
|             href="#Paragraphs" |             href="/#Paragraphs" | ||||||
|           > |           > | ||||||
|             Paragraphs |             Paragraphs | ||||||
|           </a> |           </a> | ||||||
| @@ -48111,7 +48113,7 @@ exports[`Storyshots MarkdownView Default 1`] = ` | |||||||
| 
 | 
 | ||||||
|         <li> |         <li> | ||||||
|           <a |           <a | ||||||
|             href="#Blockquotes" |             href="/#Blockquotes" | ||||||
|           > |           > | ||||||
|             Blockquotes |             Blockquotes | ||||||
|           </a> |           </a> | ||||||
| @@ -48120,7 +48122,7 @@ exports[`Storyshots MarkdownView Default 1`] = ` | |||||||
| 
 | 
 | ||||||
|         <li> |         <li> | ||||||
|           <a |           <a | ||||||
|             href="#Lists" |             href="/#Lists" | ||||||
|           > |           > | ||||||
|             Lists |             Lists | ||||||
|           </a> |           </a> | ||||||
| @@ -48129,7 +48131,7 @@ exports[`Storyshots MarkdownView Default 1`] = ` | |||||||
| 
 | 
 | ||||||
|         <li> |         <li> | ||||||
|           <a |           <a | ||||||
|             href="#Horizontal" |             href="/#Horizontal" | ||||||
|           > |           > | ||||||
|             Horizontal rule |             Horizontal rule | ||||||
|           </a> |           </a> | ||||||
| @@ -48138,7 +48140,7 @@ exports[`Storyshots MarkdownView Default 1`] = ` | |||||||
| 
 | 
 | ||||||
|         <li> |         <li> | ||||||
|           <a |           <a | ||||||
|             href="#Table" |             href="/#Table" | ||||||
|           > |           > | ||||||
|             Table |             Table | ||||||
|           </a> |           </a> | ||||||
| @@ -48147,7 +48149,7 @@ exports[`Storyshots MarkdownView Default 1`] = ` | |||||||
| 
 | 
 | ||||||
|         <li> |         <li> | ||||||
|           <a |           <a | ||||||
|             href="#Code" |             href="/#Code" | ||||||
|           > |           > | ||||||
|             Code |             Code | ||||||
|           </a> |           </a> | ||||||
| @@ -48156,7 +48158,7 @@ exports[`Storyshots MarkdownView Default 1`] = ` | |||||||
| 
 | 
 | ||||||
|         <li> |         <li> | ||||||
|           <a |           <a | ||||||
|             href="#Inline" |             href="/#Inline" | ||||||
|           > |           > | ||||||
|             Inline elements |             Inline elements | ||||||
|           </a> |           </a> | ||||||
| @@ -48254,7 +48256,7 @@ exports[`Storyshots MarkdownView Default 1`] = ` | |||||||
| 
 | 
 | ||||||
|       <p> |       <p> | ||||||
|         <a |         <a | ||||||
|           href="#top" |           href="/#top" | ||||||
|         > |         > | ||||||
|           [Top] |           [Top] | ||||||
|         </a> |         </a> | ||||||
| @@ -48289,7 +48291,7 @@ exports[`Storyshots MarkdownView Default 1`] = ` | |||||||
| 
 | 
 | ||||||
|       <p> |       <p> | ||||||
|         <a |         <a | ||||||
|           href="#top" |           href="/#top" | ||||||
|         > |         > | ||||||
|           [Top] |           [Top] | ||||||
|         </a> |         </a> | ||||||
| @@ -48362,7 +48364,7 @@ exports[`Storyshots MarkdownView Default 1`] = ` | |||||||
| 
 | 
 | ||||||
|       <p> |       <p> | ||||||
|         <a |         <a | ||||||
|           href="#top" |           href="/#top" | ||||||
|         > |         > | ||||||
|           [Top] |           [Top] | ||||||
|         </a> |         </a> | ||||||
| @@ -48508,7 +48510,7 @@ Deserunt officia esse aliquip consectetur duis ut labore laborum commodo aliquip | |||||||
| 
 | 
 | ||||||
|       <p> |       <p> | ||||||
|         <a |         <a | ||||||
|           href="#top" |           href="/#top" | ||||||
|         > |         > | ||||||
|           [Top] |           [Top] | ||||||
|         </a> |         </a> | ||||||
| @@ -48541,7 +48543,7 @@ Deserunt officia esse aliquip consectetur duis ut labore laborum commodo aliquip | |||||||
| 
 | 
 | ||||||
|       <p> |       <p> | ||||||
|         <a |         <a | ||||||
|           href="#top" |           href="/#top" | ||||||
|         > |         > | ||||||
|           [Top] |           [Top] | ||||||
|         </a> |         </a> | ||||||
| @@ -48908,7 +48910,7 @@ Deserunt officia esse aliquip consectetur duis ut labore laborum commodo aliquip | |||||||
| 
 | 
 | ||||||
|       <p> |       <p> | ||||||
|         <a |         <a | ||||||
|           href="#top" |           href="/#top" | ||||||
|         > |         > | ||||||
|           [Top] |           [Top] | ||||||
|         </a> |         </a> | ||||||
| @@ -50106,7 +50108,7 @@ Deserunt officia esse aliquip consectetur duis ut labore laborum commodo aliquip | |||||||
| 
 | 
 | ||||||
|       <p> |       <p> | ||||||
|         <a |         <a | ||||||
|           href="#top" |           href="/#top" | ||||||
|         > |         > | ||||||
|           [Top] |           [Top] | ||||||
|         </a> |         </a> | ||||||
| @@ -50135,7 +50137,7 @@ Deserunt officia esse aliquip consectetur duis ut labore laborum commodo aliquip | |||||||
|         </strong> |         </strong> | ||||||
|          reprehenderit duis  |          reprehenderit duis  | ||||||
|         <a |         <a | ||||||
|           href="#!" |           href="/#!" | ||||||
|         > |         > | ||||||
|           irure |           irure | ||||||
|         </a> |         </a> | ||||||
| @@ -50165,7 +50167,7 @@ Deserunt officia esse aliquip consectetur duis ut labore laborum commodo aliquip | |||||||
|         </code> |         </code> | ||||||
|          anim aute reprehenderit id eu ea. Aute  |          anim aute reprehenderit id eu ea. Aute  | ||||||
|         <a |         <a | ||||||
|           href="#!" |           href="/#!" | ||||||
|         > |         > | ||||||
|           excepteur proident |           excepteur proident | ||||||
|         </a> |         </a> | ||||||
| @@ -50204,6 +50206,8 @@ Deserunt officia esse aliquip consectetur duis ut labore laborum commodo aliquip | |||||||
|       <p> |       <p> | ||||||
|         <a |         <a | ||||||
|           href="https://youtu.be/s6bCmZmy9aQ" |           href="https://youtu.be/s6bCmZmy9aQ" | ||||||
|  |           rel="noopener noreferrer" | ||||||
|  |           target="_blank" | ||||||
|         > |         > | ||||||
|           <img |           <img | ||||||
|             alt="Manny Pacquiao" |             alt="Manny Pacquiao" | ||||||
| @@ -50216,7 +50220,7 @@ Deserunt officia esse aliquip consectetur duis ut labore laborum commodo aliquip | |||||||
|       <p> |       <p> | ||||||
|         Reprehenderit non eu quis in ad elit esse qui aute id  |         Reprehenderit non eu quis in ad elit esse qui aute id  | ||||||
|         <a |         <a | ||||||
|           href="#!" |           href="/#!" | ||||||
|         > |         > | ||||||
|           incididunt |           incididunt | ||||||
|         </a> |         </a> | ||||||
| @@ -58105,6 +58109,166 @@ the story is mostly for checking if the links are rendered correct. | |||||||
|       </p> |       </p> | ||||||
|        |        | ||||||
| 
 | 
 | ||||||
|  |       <h2 | ||||||
|  |         id="custom-protocol" | ||||||
|  |       > | ||||||
|  |         Custom Protocol | ||||||
|  |       </h2> | ||||||
|  |        | ||||||
|  | 
 | ||||||
|  |       <p> | ||||||
|  |         Renderers for custom protocols can be added via the "markdown-renderer.link.protocol" extension point:  | ||||||
|  |         <div | ||||||
|  |           style={ | ||||||
|  |             Object { | ||||||
|  |               "border": "1px dashed lightgray", | ||||||
|  |               "padding": "2px", | ||||||
|  |             } | ||||||
|  |           } | ||||||
|  |         > | ||||||
|  |           <h4> | ||||||
|  |             Link:  | ||||||
|  |             marvin@hitchhiker.com | ||||||
|  |              [Protocol:  | ||||||
|  |             scw | ||||||
|  |             ] | ||||||
|  |           </h4> | ||||||
|  |           <div> | ||||||
|  |             children:  | ||||||
|  |             description of scw link | ||||||
|  |           </div> | ||||||
|  |         </div> | ||||||
|  |       </p> | ||||||
|  |        | ||||||
|  | 
 | ||||||
|  |       <h2 | ||||||
|  |         id="internal" | ||||||
|  |       > | ||||||
|  |         Internal | ||||||
|  |       </h2> | ||||||
|  |        | ||||||
|  | 
 | ||||||
|  |       <p> | ||||||
|  |         Internal links should be rendered by react-router:  | ||||||
|  |         <a | ||||||
|  |           href="/scm/buttons" | ||||||
|  |           onClick={[Function]} | ||||||
|  |         > | ||||||
|  |           internal link | ||||||
|  |         </a> | ||||||
|  |       </p> | ||||||
|  |     </div> | ||||||
|  |   </div> | ||||||
|  | </div> | ||||||
|  | `; | ||||||
|  | 
 | ||||||
|  | exports[`Storyshots MarkdownView Links without Base Path 1`] = ` | ||||||
|  | <div | ||||||
|  |   className="MarkdownViewstories__Spacing-sc-1lofakk-0 jNfUaE" | ||||||
|  | > | ||||||
|  |   <div | ||||||
|  |     className="content is-word-break" | ||||||
|  |   > | ||||||
|  |     <div> | ||||||
|  |       <h1 | ||||||
|  |         id="links" | ||||||
|  |       > | ||||||
|  |         Links | ||||||
|  |       </h1> | ||||||
|  |        | ||||||
|  | 
 | ||||||
|  |       <p> | ||||||
|  |         Show case for different style of markdown links. | ||||||
|  | Please note that some of the links may not work in storybook, | ||||||
|  | the story is mostly for checking if the links are rendered correct. | ||||||
|  |       </p> | ||||||
|  |        | ||||||
|  | 
 | ||||||
|  |       <h2 | ||||||
|  |         id="external" | ||||||
|  |       > | ||||||
|  |         External | ||||||
|  |       </h2> | ||||||
|  |        | ||||||
|  | 
 | ||||||
|  |       <p> | ||||||
|  |         External Links should be opened in a new tab:  | ||||||
|  |         <a | ||||||
|  |           href="https://scm-manager.org" | ||||||
|  |           rel="noopener noreferrer" | ||||||
|  |           target="_blank" | ||||||
|  |         > | ||||||
|  |           external link | ||||||
|  |         </a> | ||||||
|  |       </p> | ||||||
|  |        | ||||||
|  | 
 | ||||||
|  |       <h2 | ||||||
|  |         id="anchor" | ||||||
|  |       > | ||||||
|  |         Anchor | ||||||
|  |       </h2> | ||||||
|  |        | ||||||
|  | 
 | ||||||
|  |       <p> | ||||||
|  |         Anchor Links should be rendered a simple a tag with an href:  | ||||||
|  |         <a | ||||||
|  |           href="/#sample" | ||||||
|  |         > | ||||||
|  |           anchor link | ||||||
|  |         </a> | ||||||
|  |       </p> | ||||||
|  |        | ||||||
|  | 
 | ||||||
|  |       <h2 | ||||||
|  |         id="protocol" | ||||||
|  |       > | ||||||
|  |         Protocol | ||||||
|  |       </h2> | ||||||
|  |        | ||||||
|  | 
 | ||||||
|  |       <p> | ||||||
|  |         Links with a protocol other than http should be rendered a simple a tag with an href e.g.:  | ||||||
|  |         <a | ||||||
|  |           href="mailto:marvin@hitchhiker.com" | ||||||
|  |         > | ||||||
|  |           mail link | ||||||
|  |         </a> | ||||||
|  |       </p> | ||||||
|  |        | ||||||
|  | 
 | ||||||
|  |       <h2 | ||||||
|  |         id="custom-protocol" | ||||||
|  |       > | ||||||
|  |         Custom Protocol | ||||||
|  |       </h2> | ||||||
|  |        | ||||||
|  | 
 | ||||||
|  |       <p> | ||||||
|  |         Renderers for custom protocols can be added via the "markdown-renderer.link.protocol" extension point:  | ||||||
|  |         <div | ||||||
|  |           style={ | ||||||
|  |             Object { | ||||||
|  |               "border": "1px dashed lightgray", | ||||||
|  |               "padding": "2px", | ||||||
|  |             } | ||||||
|  |           } | ||||||
|  |         > | ||||||
|  |           <h4> | ||||||
|  |             Link:  | ||||||
|  |             marvin@hitchhiker.com | ||||||
|  |              [Protocol:  | ||||||
|  |             scw | ||||||
|  |             ] | ||||||
|  |           </h4> | ||||||
|  |           <div> | ||||||
|  |             children:  | ||||||
|  |             description of scw link | ||||||
|  |           </div> | ||||||
|  |         </div> | ||||||
|  |       </p> | ||||||
|  |        | ||||||
|  | 
 | ||||||
|       <h2 |       <h2 | ||||||
|         id="internal" |         id="internal" | ||||||
|       > |       > | ||||||
| @@ -58116,7 +58280,6 @@ the story is mostly for checking if the links are rendered correct. | |||||||
|         Internal links should be rendered by react-router:  |         Internal links should be rendered by react-router:  | ||||||
|         <a |         <a | ||||||
|           href="/buttons" |           href="/buttons" | ||||||
|           onClick={[Function]} |  | ||||||
|         > |         > | ||||||
|           internal link |           internal link | ||||||
|         </a> |         </a> | ||||||
| @@ -58146,7 +58309,7 @@ exports[`Storyshots MarkdownView Skip Html 1`] = ` | |||||||
| 
 | 
 | ||||||
|         <li> |         <li> | ||||||
|           <a |           <a | ||||||
|             href="#Headings" |             href="/#Headings" | ||||||
|           > |           > | ||||||
|             Headings |             Headings | ||||||
|           </a> |           </a> | ||||||
| @@ -58155,7 +58318,7 @@ exports[`Storyshots MarkdownView Skip Html 1`] = ` | |||||||
| 
 | 
 | ||||||
|         <li> |         <li> | ||||||
|           <a |           <a | ||||||
|             href="#Paragraphs" |             href="/#Paragraphs" | ||||||
|           > |           > | ||||||
|             Paragraphs |             Paragraphs | ||||||
|           </a> |           </a> | ||||||
| @@ -58164,7 +58327,7 @@ exports[`Storyshots MarkdownView Skip Html 1`] = ` | |||||||
| 
 | 
 | ||||||
|         <li> |         <li> | ||||||
|           <a |           <a | ||||||
|             href="#Blockquotes" |             href="/#Blockquotes" | ||||||
|           > |           > | ||||||
|             Blockquotes |             Blockquotes | ||||||
|           </a> |           </a> | ||||||
| @@ -58173,7 +58336,7 @@ exports[`Storyshots MarkdownView Skip Html 1`] = ` | |||||||
| 
 | 
 | ||||||
|         <li> |         <li> | ||||||
|           <a |           <a | ||||||
|             href="#Lists" |             href="/#Lists" | ||||||
|           > |           > | ||||||
|             Lists |             Lists | ||||||
|           </a> |           </a> | ||||||
| @@ -58182,7 +58345,7 @@ exports[`Storyshots MarkdownView Skip Html 1`] = ` | |||||||
| 
 | 
 | ||||||
|         <li> |         <li> | ||||||
|           <a |           <a | ||||||
|             href="#Horizontal" |             href="/#Horizontal" | ||||||
|           > |           > | ||||||
|             Horizontal rule |             Horizontal rule | ||||||
|           </a> |           </a> | ||||||
| @@ -58191,7 +58354,7 @@ exports[`Storyshots MarkdownView Skip Html 1`] = ` | |||||||
| 
 | 
 | ||||||
|         <li> |         <li> | ||||||
|           <a |           <a | ||||||
|             href="#Table" |             href="/#Table" | ||||||
|           > |           > | ||||||
|             Table |             Table | ||||||
|           </a> |           </a> | ||||||
| @@ -58200,7 +58363,7 @@ exports[`Storyshots MarkdownView Skip Html 1`] = ` | |||||||
| 
 | 
 | ||||||
|         <li> |         <li> | ||||||
|           <a |           <a | ||||||
|             href="#Code" |             href="/#Code" | ||||||
|           > |           > | ||||||
|             Code |             Code | ||||||
|           </a> |           </a> | ||||||
| @@ -58209,7 +58372,7 @@ exports[`Storyshots MarkdownView Skip Html 1`] = ` | |||||||
| 
 | 
 | ||||||
|         <li> |         <li> | ||||||
|           <a |           <a | ||||||
|             href="#Inline" |             href="/#Inline" | ||||||
|           > |           > | ||||||
|             Inline elements |             Inline elements | ||||||
|           </a> |           </a> | ||||||
| @@ -58303,7 +58466,7 @@ exports[`Storyshots MarkdownView Skip Html 1`] = ` | |||||||
| 
 | 
 | ||||||
|       <p> |       <p> | ||||||
|         <a |         <a | ||||||
|           href="#top" |           href="/#top" | ||||||
|         > |         > | ||||||
|           [Top] |           [Top] | ||||||
|         </a> |         </a> | ||||||
| @@ -58334,7 +58497,7 @@ exports[`Storyshots MarkdownView Skip Html 1`] = ` | |||||||
| 
 | 
 | ||||||
|       <p> |       <p> | ||||||
|         <a |         <a | ||||||
|           href="#top" |           href="/#top" | ||||||
|         > |         > | ||||||
|           [Top] |           [Top] | ||||||
|         </a> |         </a> | ||||||
| @@ -58403,7 +58566,7 @@ exports[`Storyshots MarkdownView Skip Html 1`] = ` | |||||||
| 
 | 
 | ||||||
|       <p> |       <p> | ||||||
|         <a |         <a | ||||||
|           href="#top" |           href="/#top" | ||||||
|         > |         > | ||||||
|           [Top] |           [Top] | ||||||
|         </a> |         </a> | ||||||
| @@ -58547,7 +58710,7 @@ Deserunt officia esse aliquip consectetur duis ut labore laborum commodo aliquip | |||||||
| 
 | 
 | ||||||
|       <p> |       <p> | ||||||
|         <a |         <a | ||||||
|           href="#top" |           href="/#top" | ||||||
|         > |         > | ||||||
|           [Top] |           [Top] | ||||||
|         </a> |         </a> | ||||||
| @@ -58576,7 +58739,7 @@ Deserunt officia esse aliquip consectetur duis ut labore laborum commodo aliquip | |||||||
| 
 | 
 | ||||||
|       <p> |       <p> | ||||||
|         <a |         <a | ||||||
|           href="#top" |           href="/#top" | ||||||
|         > |         > | ||||||
|           [Top] |           [Top] | ||||||
|         </a> |         </a> | ||||||
| @@ -58986,7 +59149,7 @@ Deserunt officia esse aliquip consectetur duis ut labore laborum commodo aliquip | |||||||
| 
 | 
 | ||||||
|       <p> |       <p> | ||||||
|         <a |         <a | ||||||
|           href="#top" |           href="/#top" | ||||||
|         > |         > | ||||||
|           [Top] |           [Top] | ||||||
|         </a> |         </a> | ||||||
| @@ -60180,7 +60343,7 @@ Deserunt officia esse aliquip consectetur duis ut labore laborum commodo aliquip | |||||||
| 
 | 
 | ||||||
|       <p> |       <p> | ||||||
|         <a |         <a | ||||||
|           href="#top" |           href="/#top" | ||||||
|         > |         > | ||||||
|           [Top] |           [Top] | ||||||
|         </a> |         </a> | ||||||
| @@ -60205,7 +60368,7 @@ Deserunt officia esse aliquip consectetur duis ut labore laborum commodo aliquip | |||||||
|         </strong> |         </strong> | ||||||
|          reprehenderit duis  |          reprehenderit duis  | ||||||
|         <a |         <a | ||||||
|           href="#!" |           href="/#!" | ||||||
|         > |         > | ||||||
|           irure |           irure | ||||||
|         </a> |         </a> | ||||||
| @@ -60235,7 +60398,7 @@ Deserunt officia esse aliquip consectetur duis ut labore laborum commodo aliquip | |||||||
|         </code> |         </code> | ||||||
|          anim aute reprehenderit id eu ea. Aute  |          anim aute reprehenderit id eu ea. Aute  | ||||||
|         <a |         <a | ||||||
|           href="#!" |           href="/#!" | ||||||
|         > |         > | ||||||
|           excepteur proident |           excepteur proident | ||||||
|         </a> |         </a> | ||||||
| @@ -60274,6 +60437,8 @@ Deserunt officia esse aliquip consectetur duis ut labore laborum commodo aliquip | |||||||
|       <p> |       <p> | ||||||
|         <a |         <a | ||||||
|           href="https://youtu.be/s6bCmZmy9aQ" |           href="https://youtu.be/s6bCmZmy9aQ" | ||||||
|  |           rel="noopener noreferrer" | ||||||
|  |           target="_blank" | ||||||
|         > |         > | ||||||
|           <img |           <img | ||||||
|             alt="Manny Pacquiao" |             alt="Manny Pacquiao" | ||||||
| @@ -60286,7 +60451,7 @@ Deserunt officia esse aliquip consectetur duis ut labore laborum commodo aliquip | |||||||
|       <p> |       <p> | ||||||
|         Reprehenderit non eu quis in ad elit esse qui aute id  |         Reprehenderit non eu quis in ad elit esse qui aute id  | ||||||
|         <a |         <a | ||||||
|           href="#!" |           href="/#!" | ||||||
|         > |         > | ||||||
|           incididunt |           incididunt | ||||||
|         </a> |         </a> | ||||||
|   | |||||||
| @@ -95,6 +95,7 @@ export * from "./repos"; | |||||||
| export * from "./table"; | export * from "./table"; | ||||||
| export * from "./toast"; | export * from "./toast"; | ||||||
| export * from "./popover"; | export * from "./popover"; | ||||||
|  | export * from "./markdown/markdownExtensions"; | ||||||
|  |  | ||||||
| export { | export { | ||||||
|   File, |   File, | ||||||
|   | |||||||
| @@ -57,19 +57,19 @@ describe("test isExternalLink", () => { | |||||||
|  |  | ||||||
| describe("test isLinkWithProtocol", () => { | describe("test isLinkWithProtocol", () => { | ||||||
|   it("should return true", () => { |   it("should return true", () => { | ||||||
|     expect(isLinkWithProtocol("ldap://[2001:db8::7]/c=GB?objectClass?one")).toBe(true); |     expect(isLinkWithProtocol("ldap://[2001:db8::7]/c=GB?objectClass?one")).toBeTruthy(); | ||||||
|     expect(isLinkWithProtocol("mailto:trillian@hitchhiker.com")).toBe(true); |     expect(isLinkWithProtocol("mailto:trillian@hitchhiker.com")).toBeTruthy(); | ||||||
|     expect(isLinkWithProtocol("tel:+1-816-555-1212")).toBe(true); |     expect(isLinkWithProtocol("tel:+1-816-555-1212")).toBeTruthy(); | ||||||
|     expect(isLinkWithProtocol("urn:oasis:names:specification:docbook:dtd:xml:4.1.2")).toBe(true); |     expect(isLinkWithProtocol("urn:oasis:names:specification:docbook:dtd:xml:4.1.2")).toBeTruthy(); | ||||||
|     expect(isLinkWithProtocol("about:config")).toBe(true); |     expect(isLinkWithProtocol("about:config")).toBeTruthy(); | ||||||
|     expect(isLinkWithProtocol("http://cloudogu.com")).toBe(true); |     expect(isLinkWithProtocol("http://cloudogu.com")).toBeTruthy(); | ||||||
|     expect(isLinkWithProtocol("file:///srv/git/project.git")).toBe(true); |     expect(isLinkWithProtocol("file:///srv/git/project.git")).toBeTruthy(); | ||||||
|     expect(isLinkWithProtocol("ssh://trillian@server/project.git")).toBe(true); |     expect(isLinkWithProtocol("ssh://trillian@server/project.git")).toBeTruthy(); | ||||||
|   }); |   }); | ||||||
|   it("should return false", () => { |   it("should return false", () => { | ||||||
|     expect(isLinkWithProtocol("some/path/link")).toBe(false); |     expect(isLinkWithProtocol("some/path/link")).toBeFalsy(); | ||||||
|     expect(isLinkWithProtocol("/some/path/link")).toBe(false); |     expect(isLinkWithProtocol("/some/path/link")).toBeFalsy(); | ||||||
|     expect(isLinkWithProtocol("#some-anchor")).toBe(false); |     expect(isLinkWithProtocol("#some-anchor")).toBeFalsy(); | ||||||
|   }); |   }); | ||||||
| }); | }); | ||||||
|  |  | ||||||
|   | |||||||
| @@ -25,6 +25,7 @@ import React, { FC } from "react"; | |||||||
| import { Link, useLocation } from "react-router-dom"; | import { Link, useLocation } from "react-router-dom"; | ||||||
| import ExternalLink from "../navigation/ExternalLink"; | import ExternalLink from "../navigation/ExternalLink"; | ||||||
| import { urls } from "@scm-manager/ui-api"; | import { urls } from "@scm-manager/ui-api"; | ||||||
|  | import { ProtocolLinkRendererExtensionMap } from "./markdownExtensions"; | ||||||
|  |  | ||||||
| const externalLinkRegex = new RegExp("^http(s)?://"); | const externalLinkRegex = new RegExp("^http(s)?://"); | ||||||
| export const isExternalLink = (link: string) => { | export const isExternalLink = (link: string) => { | ||||||
| @@ -39,9 +40,10 @@ export const isInternalScmRepoLink = (link: string) => { | |||||||
|   return link.startsWith("/repo/"); |   return link.startsWith("/repo/"); | ||||||
| }; | }; | ||||||
|  |  | ||||||
| const linkWithProtcolRegex = new RegExp("^[a-z]+:"); | const linkWithProtocolRegex = new RegExp("^([a-z]+):(.+)"); | ||||||
| export const isLinkWithProtocol = (link: string) => { | export const isLinkWithProtocol = (link: string) => { | ||||||
|   return linkWithProtcolRegex.test(link); |   const match = link.match(linkWithProtocolRegex); | ||||||
|  |   return match && { protocol: match[1], link: match[2] }; | ||||||
| }; | }; | ||||||
|  |  | ||||||
| const join = (left: string, right: string) => { | const join = (left: string, right: string) => { | ||||||
| @@ -106,10 +108,10 @@ type LinkProps = { | |||||||
| }; | }; | ||||||
|  |  | ||||||
| type Props = LinkProps & { | type Props = LinkProps & { | ||||||
|   base: string; |   base?: string; | ||||||
| }; | }; | ||||||
|  |  | ||||||
| const MarkdownLinkRenderer: FC<Props> = ({ href, base, children }) => { | const MarkdownLinkRenderer: FC<Props> = ({ href = "", base, children, ...props }) => { | ||||||
|   const location = useLocation(); |   const location = useLocation(); | ||||||
|   if (isExternalLink(href)) { |   if (isExternalLink(href)) { | ||||||
|     return <ExternalLink to={href}>{children}</ExternalLink>; |     return <ExternalLink to={href}>{children}</ExternalLink>; | ||||||
| @@ -117,16 +119,39 @@ const MarkdownLinkRenderer: FC<Props> = ({ href, base, children }) => { | |||||||
|     return <a href={href}>{children}</a>; |     return <a href={href}>{children}</a>; | ||||||
|   } else if (isAnchorLink(href)) { |   } else if (isAnchorLink(href)) { | ||||||
|     return <a href={urls.withContextPath(location.pathname) + href}>{children}</a>; |     return <a href={urls.withContextPath(location.pathname) + href}>{children}</a>; | ||||||
|   } else { |   } else if (base) { | ||||||
|     const localLink = createLocalLink(base, location.pathname, href); |     const localLink = createLocalLink(base, location.pathname, href); | ||||||
|     return <Link to={localLink}>{children}</Link>; |     return <Link to={localLink}>{children}</Link>; | ||||||
|  |   } else if (href) { | ||||||
|  |     return ( | ||||||
|  |       <a href={href} {...props}> | ||||||
|  |         {children} | ||||||
|  |       </a> | ||||||
|  |     ); | ||||||
|  |   } else { | ||||||
|  |     return <a {...props}>{children}</a>; | ||||||
|   } |   } | ||||||
| }; | }; | ||||||
|  |  | ||||||
| // we use a factory method, because react-markdown does not pass | // we use a factory method, because react-markdown does not pass | ||||||
| // base as prop down to our link component. | // base as prop down to our link component. | ||||||
| export const create = (base: string): FC<LinkProps> => { | export const create = (base?: string, protocolExtensions: ProtocolLinkRendererExtensionMap = {}): FC<LinkProps> => { | ||||||
|   return props => <MarkdownLinkRenderer base={base} {...props} />; |   return props => { | ||||||
|  |     const protocolLinkContext = isLinkWithProtocol(props.href || ""); | ||||||
|  |     if (protocolLinkContext) { | ||||||
|  |       const { link, protocol } = protocolLinkContext; | ||||||
|  |       const ProtocolRenderer = protocolExtensions[protocol]; | ||||||
|  |       if (ProtocolRenderer) { | ||||||
|  |         return ( | ||||||
|  |           <ProtocolRenderer protocol={protocol} href={link}> | ||||||
|  |             {props.children} | ||||||
|  |           </ProtocolRenderer> | ||||||
|  |         ); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     return <MarkdownLinkRenderer base={base} {...props} />; | ||||||
|  |   }; | ||||||
| }; | }; | ||||||
|  |  | ||||||
| export default MarkdownLinkRenderer; | export default MarkdownLinkRenderer; | ||||||
|   | |||||||
| @@ -39,6 +39,7 @@ import Title from "../layout/Title"; | |||||||
| import { Subtitle } from "../layout"; | import { Subtitle } from "../layout"; | ||||||
| import { MemoryRouter } from "react-router-dom"; | import { MemoryRouter } from "react-router-dom"; | ||||||
| import { Binder, BinderContext } from "@scm-manager/ui-extensions"; | import { Binder, BinderContext } from "@scm-manager/ui-extensions"; | ||||||
|  | import { ProtocolLinkRendererExtension, ProtocolLinkRendererProps } from "./markdownExtensions"; | ||||||
|  |  | ||||||
| const Spacing = styled.div` | const Spacing = styled.div` | ||||||
|   padding: 2em; |   padding: 2em; | ||||||
| @@ -58,7 +59,30 @@ storiesOf("MarkdownView", module) | |||||||
|       <MarkdownView content={MarkdownInlineXml} /> |       <MarkdownView content={MarkdownInlineXml} /> | ||||||
|     </> |     </> | ||||||
|   )) |   )) | ||||||
|   .add("Links", () => <MarkdownView content={MarkdownLinks} basePath="/" />) |   .add("Links", () => { | ||||||
|  |     const binder = new Binder("custom protocol link renderer"); | ||||||
|  |     binder.bind("markdown-renderer.link.protocol", { | ||||||
|  |       protocol: "scw", | ||||||
|  |       renderer: ProtocolLinkRenderer | ||||||
|  |     } as ProtocolLinkRendererExtension); | ||||||
|  |     return ( | ||||||
|  |       <BinderContext.Provider value={binder}> | ||||||
|  |         <MarkdownView content={MarkdownLinks} basePath="/scm/" /> | ||||||
|  |       </BinderContext.Provider> | ||||||
|  |     ); | ||||||
|  |   }) | ||||||
|  |   .add("Links without Base Path", () => { | ||||||
|  |     const binder = new Binder("custom protocol link renderer"); | ||||||
|  |     binder.bind("markdown-renderer.link.protocol", { | ||||||
|  |       protocol: "scw", | ||||||
|  |       renderer: ProtocolLinkRenderer | ||||||
|  |     } as ProtocolLinkRendererExtension); | ||||||
|  |     return ( | ||||||
|  |       <BinderContext.Provider value={binder}> | ||||||
|  |         <MarkdownView content={MarkdownLinks} /> | ||||||
|  |       </BinderContext.Provider> | ||||||
|  |     ); | ||||||
|  |   }) | ||||||
|   .add("Header Anchor Links", () => ( |   .add("Header Anchor Links", () => ( | ||||||
|     <MarkdownView |     <MarkdownView | ||||||
|       content={MarkdownChangelog} |       content={MarkdownChangelog} | ||||||
| @@ -88,3 +112,14 @@ storiesOf("MarkdownView", module) | |||||||
|     ); |     ); | ||||||
|   }) |   }) | ||||||
|   .add("XSS Prevention", () => <MarkdownView content={MarkdownXss} skipHtml={false} />); |   .add("XSS Prevention", () => <MarkdownView content={MarkdownXss} skipHtml={false} />); | ||||||
|  |  | ||||||
|  | export const ProtocolLinkRenderer: FC<ProtocolLinkRendererProps> = ({ protocol, href, children }) => { | ||||||
|  |   return ( | ||||||
|  |     <div style={{ border: "1px dashed lightgray", padding: "2px" }}> | ||||||
|  |       <h4> | ||||||
|  |         Link: {href} [Protocol: {protocol}] | ||||||
|  |       </h4> | ||||||
|  |       <div>children: {children}</div> | ||||||
|  |     </div> | ||||||
|  |   ); | ||||||
|  | }; | ||||||
|   | |||||||
| @@ -29,7 +29,7 @@ import sanitize from "rehype-sanitize"; | |||||||
| import remark2rehype from "remark-rehype"; | import remark2rehype from "remark-rehype"; | ||||||
| import rehype2react from "rehype-react"; | import rehype2react from "rehype-react"; | ||||||
| import gfm from "remark-gfm"; | import gfm from "remark-gfm"; | ||||||
| import { binder } from "@scm-manager/ui-extensions"; | import { BinderContext } from "@scm-manager/ui-extensions"; | ||||||
| import ErrorBoundary from "../ErrorBoundary"; | import ErrorBoundary from "../ErrorBoundary"; | ||||||
| import { create as createMarkdownHeadingRenderer } from "./MarkdownHeadingRenderer"; | import { create as createMarkdownHeadingRenderer } from "./MarkdownHeadingRenderer"; | ||||||
| import { create as createMarkdownLinkRenderer } from "./MarkdownLinkRenderer"; | import { create as createMarkdownLinkRenderer } from "./MarkdownLinkRenderer"; | ||||||
| @@ -46,6 +46,7 @@ import raw from "rehype-raw"; | |||||||
| import slug from "rehype-slug"; | import slug from "rehype-slug"; | ||||||
| import merge from "deepmerge"; | import merge from "deepmerge"; | ||||||
| import { createComponentList } from "./createComponentList"; | import { createComponentList } from "./createComponentList"; | ||||||
|  | import { ProtocolLinkRendererExtension, ProtocolLinkRendererExtensionMap } from "./markdownExtensions"; | ||||||
|  |  | ||||||
| type Props = RouteComponentProps & | type Props = RouteComponentProps & | ||||||
|   WithTranslation & { |   WithTranslation & { | ||||||
| @@ -94,6 +95,8 @@ const MarkdownErrorNotification: FC = () => { | |||||||
| }; | }; | ||||||
|  |  | ||||||
| class MarkdownView extends React.Component<Props, State> { | class MarkdownView extends React.Component<Props, State> { | ||||||
|  |   static contextType = BinderContext; | ||||||
|  |  | ||||||
|   static defaultProps: Partial<Props> = { |   static defaultProps: Partial<Props> = { | ||||||
|     enableAnchorHeadings: false, |     enableAnchorHeadings: false, | ||||||
|     skipHtml: false |     skipHtml: false | ||||||
| @@ -143,7 +146,7 @@ class MarkdownView extends React.Component<Props, State> { | |||||||
|       mdastPlugins = [] |       mdastPlugins = [] | ||||||
|     } = this.props; |     } = this.props; | ||||||
|  |  | ||||||
|     const rendererFactory = binder.getExtension("markdown-renderer-factory"); |     const rendererFactory = this.context.getExtension("markdown-renderer-factory"); | ||||||
|     let remarkRendererList = renderers; |     let remarkRendererList = renderers; | ||||||
|  |  | ||||||
|     if (rendererFactory) { |     if (rendererFactory) { | ||||||
| @@ -158,8 +161,19 @@ class MarkdownView extends React.Component<Props, State> { | |||||||
|       remarkRendererList.heading = createMarkdownHeadingRenderer(permalink); |       remarkRendererList.heading = createMarkdownHeadingRenderer(permalink); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     if (basePath && !remarkRendererList.link) { |     let protocolLinkRendererExtensions: ProtocolLinkRendererExtensionMap = {}; | ||||||
|       remarkRendererList.link = createMarkdownLinkRenderer(basePath); |     if (!remarkRendererList.link) { | ||||||
|  |       const extensionPoints = this.context.getExtensions( | ||||||
|  |         "markdown-renderer.link.protocol" | ||||||
|  |       ) as ProtocolLinkRendererExtension[]; | ||||||
|  |       protocolLinkRendererExtensions = extensionPoints.reduce<ProtocolLinkRendererExtensionMap>( | ||||||
|  |         (prev, { protocol, renderer }) => { | ||||||
|  |           prev[protocol] = renderer; | ||||||
|  |           return prev; | ||||||
|  |         }, | ||||||
|  |         {} | ||||||
|  |       ); | ||||||
|  |       remarkRendererList.link = createMarkdownLinkRenderer(basePath, protocolLinkRendererExtensions); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     if (!remarkRendererList.code) { |     if (!remarkRendererList.code) { | ||||||
| @@ -188,7 +202,10 @@ class MarkdownView extends React.Component<Props, State> { | |||||||
|           attributes: { |           attributes: { | ||||||
|             code: ["className"] // Allow className for code elements, this is necessary to extract the code language |             code: ["className"] // Allow className for code elements, this is necessary to extract the code language | ||||||
|           }, |           }, | ||||||
|           clobberPrefix: "" // Do not prefix user-provided ids and class names |           clobberPrefix: "", // Do not prefix user-provided ids and class names, | ||||||
|  |           protocols: { | ||||||
|  |             href: Object.keys(protocolLinkRendererExtensions) | ||||||
|  |           } | ||||||
|         }) |         }) | ||||||
|       ) |       ) | ||||||
|       .use(rehype2react, { |       .use(rehype2react, { | ||||||
|   | |||||||
							
								
								
									
										15
									
								
								scm-ui/ui-components/src/markdown/markdownExtensions.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								scm-ui/ui-components/src/markdown/markdownExtensions.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,15 @@ | |||||||
|  | import { FC } from "react"; | ||||||
|  |  | ||||||
|  | export type ProtocolLinkRendererProps = { | ||||||
|  |   protocol: string; | ||||||
|  |   href: string; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export type ProtocolLinkRendererExtension = { | ||||||
|  |   protocol: string; | ||||||
|  |   renderer: FC<ProtocolLinkRendererProps>; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export type ProtocolLinkRendererExtensionMap = { | ||||||
|  |   [protocol: string]: FC<ProtocolLinkRendererProps>; | ||||||
|  | } | ||||||
| @@ -48,11 +48,8 @@ export const createRemark2RehypeCodeRendererAdapter = (remarkRenderer: any) => { | |||||||
|  |  | ||||||
| export const createRemark2RehypeLinkRendererAdapter = (remarkRenderer: any) => { | export const createRemark2RehypeLinkRendererAdapter = (remarkRenderer: any) => { | ||||||
|   return ({ node, children }: any) => { |   return ({ node, children }: any) => { | ||||||
|     const renderProps = { |  | ||||||
|       href: node.properties.href || "" |  | ||||||
|     }; |  | ||||||
|     children = children || []; |     children = children || []; | ||||||
|     return React.createElement(remarkRenderer, renderProps, ...children); |     return React.createElement(remarkRenderer, node.properties, ...children); | ||||||
|   }; |   }; | ||||||
| }; | }; | ||||||
|  |  | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user