mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-27 00:23:41 +09:00 
			
		
		
		
	| @@ -91,6 +91,7 @@ module.exports = { | ||||
|       plugins: ['@vitest/eslint-plugin'], | ||||
|       globals: vitestPlugin.environments.env.globals, | ||||
|       rules: { | ||||
|         'github/unescaped-html-literal': [0], | ||||
|         '@vitest/consistent-test-filename': [0], | ||||
|         '@vitest/consistent-test-it': [0], | ||||
|         '@vitest/expect-expect': [0], | ||||
| @@ -423,7 +424,7 @@ module.exports = { | ||||
|     'github/no-useless-passive': [2], | ||||
|     'github/prefer-observers': [2], | ||||
|     'github/require-passive-events': [2], | ||||
|     'github/unescaped-html-literal': [0], | ||||
|     'github/unescaped-html-literal': [2], | ||||
|     'grouped-accessor-pairs': [2], | ||||
|     'guard-for-in': [0], | ||||
|     'id-blacklist': [0], | ||||
|   | ||||
							
								
								
									
										13
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										13
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							| @@ -28,7 +28,6 @@ | ||||
|         "dropzone": "6.0.0-beta.2", | ||||
|         "easymde": "2.20.0", | ||||
|         "esbuild-loader": "4.3.0", | ||||
|         "escape-goat": "4.0.0", | ||||
|         "fast-glob": "3.3.3", | ||||
|         "htmx.org": "2.0.6", | ||||
|         "idiomorph": "0.7.3", | ||||
| @@ -6563,18 +6562,6 @@ | ||||
|         "node": ">=6" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/escape-goat": { | ||||
|       "version": "4.0.0", | ||||
|       "resolved": "https://registry.npmjs.org/escape-goat/-/escape-goat-4.0.0.tgz", | ||||
|       "integrity": "sha512-2Sd4ShcWxbx6OY1IHyla/CVNwvg7XwZVoXZHcSu9w9SReNP1EzzD5T8NWKIR38fIqEns9kDWKUQTXXAmlDrdPg==", | ||||
|       "license": "MIT", | ||||
|       "engines": { | ||||
|         "node": ">=12" | ||||
|       }, | ||||
|       "funding": { | ||||
|         "url": "https://github.com/sponsors/sindresorhus" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/escape-string-regexp": { | ||||
|       "version": "4.0.0", | ||||
|       "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", | ||||
|   | ||||
| @@ -27,7 +27,6 @@ | ||||
|     "dropzone": "6.0.0-beta.2", | ||||
|     "easymde": "2.20.0", | ||||
|     "esbuild-loader": "4.3.0", | ||||
|     "escape-goat": "4.0.0", | ||||
|     "fast-glob": "3.3.3", | ||||
|     "htmx.org": "2.0.6", | ||||
|     "idiomorph": "0.7.3", | ||||
|   | ||||
| @@ -2,6 +2,7 @@ | ||||
| // to make sure the error handler always works, we should never import `window.config`, because | ||||
| // some user's custom template breaks it. | ||||
| import type {Intent} from './types.ts'; | ||||
| import {html} from './utils/html.ts'; | ||||
|  | ||||
| // This sets up the URL prefix used in webpack's chunk loading. | ||||
| // This file must be imported before any lazy-loading is being attempted. | ||||
| @@ -23,7 +24,7 @@ export function showGlobalErrorMessage(msg: string, msgType: Intent = 'error') { | ||||
|   let msgDiv = msgContainer.querySelector<HTMLDivElement>(`.js-global-error[data-global-error-msg-compact="${msgCompact}"]`); | ||||
|   if (!msgDiv) { | ||||
|     const el = document.createElement('div'); | ||||
|     el.innerHTML = `<div class="ui container js-global-error tw-my-[--page-spacing]"><div class="ui ${msgType} message tw-text-center tw-whitespace-pre-line"></div></div>`; | ||||
|     el.innerHTML = html`<div class="ui container js-global-error tw-my-[--page-spacing]"><div class="ui ${msgType} message tw-text-center tw-whitespace-pre-line"></div></div>`; | ||||
|     msgDiv = el.childNodes[0] as HTMLDivElement; | ||||
|   } | ||||
|   // merge duplicated messages into "the message (count)" format | ||||
|   | ||||
| @@ -2,6 +2,7 @@ import {reactive} from 'vue'; | ||||
| import {GET} from '../modules/fetch.ts'; | ||||
| import {pathEscapeSegments} from '../utils/url.ts'; | ||||
| import {createElementFromHTML} from '../utils/dom.ts'; | ||||
| import {html} from '../utils/html.ts'; | ||||
|  | ||||
| export function createViewFileTreeStore(props: { repoLink: string, treePath: string, currentRefNameSubURL: string}) { | ||||
|   const store = reactive({ | ||||
| @@ -16,7 +17,7 @@ export function createViewFileTreeStore(props: { repoLink: string, treePath: str | ||||
|         if (!document.querySelector(`.global-svg-icon-pool #${svgId}`)) poolSvgs.push(svgContent); | ||||
|       } | ||||
|       if (poolSvgs.length) { | ||||
|         const svgContainer = createElementFromHTML('<div class="global-svg-icon-pool tw-hidden"></div>'); | ||||
|         const svgContainer = createElementFromHTML(html`<div class="global-svg-icon-pool tw-hidden"></div>`); | ||||
|         svgContainer.innerHTML = poolSvgs.join(''); | ||||
|         document.body.append(svgContainer); | ||||
|       } | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| import {svg} from '../../svg.ts'; | ||||
| import {htmlEscape} from 'escape-goat'; | ||||
| import {html, htmlRaw} from '../../utils/html.ts'; | ||||
| import {createElementFromHTML} from '../../utils/dom.ts'; | ||||
| import {fomanticQuery} from '../../modules/fomantic/base.ts'; | ||||
|  | ||||
| @@ -12,17 +12,17 @@ type ConfirmModalOptions = { | ||||
| } | ||||
|  | ||||
| export function createConfirmModal({header = '', content = '', confirmButtonColor = 'primary'}:ConfirmModalOptions = {}): HTMLElement { | ||||
|   const headerHtml = header ? `<div class="header">${htmlEscape(header)}</div>` : ''; | ||||
|   return createElementFromHTML(` | ||||
| <div class="ui g-modal-confirm modal"> | ||||
|   ${headerHtml} | ||||
|   <div class="content">${htmlEscape(content)}</div> | ||||
|   <div class="actions"> | ||||
|     <button class="ui cancel button">${svg('octicon-x')} ${htmlEscape(i18n.modal_cancel)}</button> | ||||
|     <button class="ui ${confirmButtonColor} ok button">${svg('octicon-check')} ${htmlEscape(i18n.modal_confirm)}</button> | ||||
|   </div> | ||||
| </div> | ||||
| `); | ||||
|   const headerHtml = header ? html`<div class="header">${header}</div>` : ''; | ||||
|   return createElementFromHTML(html` | ||||
|     <div class="ui g-modal-confirm modal"> | ||||
|       ${htmlRaw(headerHtml)} | ||||
|       <div class="content">${content}</div> | ||||
|       <div class="actions"> | ||||
|         <button class="ui cancel button">${htmlRaw(svg('octicon-x'))} ${i18n.modal_cancel}</button> | ||||
|         <button class="ui ${confirmButtonColor} ok button">${htmlRaw(svg('octicon-check'))} ${i18n.modal_confirm}</button> | ||||
|       </div> | ||||
|     </div> | ||||
|   `.trim()); | ||||
| } | ||||
|  | ||||
| export function confirmModal(modal: HTMLElement | ConfirmModalOptions): Promise<boolean> { | ||||
|   | ||||
| @@ -114,7 +114,7 @@ async function handleUploadFiles(editor: CodeMirrorEditor | TextareaEditor, drop | ||||
|  | ||||
| export function removeAttachmentLinksFromMarkdown(text: string, fileUuid: string) { | ||||
|   text = text.replace(new RegExp(`!?\\[([^\\]]+)\\]\\(/?attachments/${fileUuid}\\)`, 'g'), ''); | ||||
|   text = text.replace(new RegExp(`<img[^>]+src="/?attachments/${fileUuid}"[^>]*>`, 'g'), ''); | ||||
|   text = text.replace(new RegExp(`[<]img[^>]+src="/?attachments/${fileUuid}"[^>]*>`, 'g'), ''); | ||||
|   return text; | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| import {htmlEscape} from 'escape-goat'; | ||||
| import {htmlEscape} from '../../utils/html.ts'; | ||||
| import {fomanticQuery} from '../../modules/fomantic/base.ts'; | ||||
|  | ||||
| const {appSubUrl} = window.config; | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| import {svg} from '../svg.ts'; | ||||
| import {htmlEscape} from 'escape-goat'; | ||||
| import {html} from '../utils/html.ts'; | ||||
| import {clippie} from 'clippie'; | ||||
| import {showTemporaryTooltip} from '../modules/tippy.ts'; | ||||
| import {GET, POST} from '../modules/fetch.ts'; | ||||
| @@ -33,14 +33,14 @@ export function generateMarkdownLinkForAttachment(file: Partial<CustomDropzoneFi | ||||
|       // Scale down images from HiDPI monitors. This uses the <img> tag because it's the only | ||||
|       // method to change image size in Markdown that is supported by all implementations. | ||||
|       // Make the image link relative to the repo path, then the final URL is "/sub-path/owner/repo/attachments/{uuid}" | ||||
|       fileMarkdown = `<img width="${Math.round(width / dppx)}" alt="${htmlEscape(file.name)}" src="attachments/${htmlEscape(file.uuid)}">`; | ||||
|       fileMarkdown = html`<img width="${Math.round(width / dppx)}" alt="${file.name}" src="attachments/${file.uuid}">`; | ||||
|     } else { | ||||
|       // Markdown always renders the image with a relative path, so the final URL is "/sub-path/owner/repo/attachments/{uuid}" | ||||
|       // TODO: it should also use relative path for consistency, because absolute is ambiguous for "/sub-path/attachments" or "/attachments" | ||||
|       fileMarkdown = ``; | ||||
|     } | ||||
|   } else if (isVideoFile(file)) { | ||||
|     fileMarkdown = `<video src="attachments/${htmlEscape(file.uuid)}" title="${htmlEscape(file.name)}" controls></video>`; | ||||
|     fileMarkdown = html`<video src="attachments/${file.uuid}" title="${file.name}" controls></video>`; | ||||
|   } | ||||
|   return fileMarkdown; | ||||
| } | ||||
|   | ||||
| @@ -1,4 +1,5 @@ | ||||
| import emojis from '../../../assets/emoji.json' with {type: 'json'}; | ||||
| import {html} from '../utils/html.ts'; | ||||
|  | ||||
| const {assetUrlPrefix, customEmojis} = window.config; | ||||
|  | ||||
| @@ -24,12 +25,11 @@ for (const key of emojiKeys) { | ||||
| export function emojiHTML(name: string) { | ||||
|   let inner; | ||||
|   if (Object.hasOwn(customEmojis, name)) { | ||||
|     inner = `<img alt=":${name}:" src="${assetUrlPrefix}/img/emoji/${name}.png">`; | ||||
|     inner = html`<img alt=":${name}:" src="${assetUrlPrefix}/img/emoji/${name}.png">`; | ||||
|   } else { | ||||
|     inner = emojiString(name); | ||||
|   } | ||||
|  | ||||
|   return `<span class="emoji" title=":${name}:">${inner}</span>`; | ||||
|   return html`<span class="emoji" title=":${name}:">${inner}</span>`; | ||||
| } | ||||
|  | ||||
| // retrieve string for given emoji name | ||||
|   | ||||
| @@ -3,7 +3,7 @@ import {newRenderPlugin3DViewer} from '../render/plugins/3d-viewer.ts'; | ||||
| import {newRenderPluginPdfViewer} from '../render/plugins/pdf-viewer.ts'; | ||||
| import {registerGlobalInitFunc} from '../modules/observer.ts'; | ||||
| import {createElementFromHTML, showElem, toggleClass} from '../utils/dom.ts'; | ||||
| import {htmlEscape} from 'escape-goat'; | ||||
| import {html} from '../utils/html.ts'; | ||||
| import {basename} from '../utils.ts'; | ||||
|  | ||||
| const plugins: FileRenderPlugin[] = []; | ||||
| @@ -54,7 +54,7 @@ async function renderRawFileToContainer(container: HTMLElement, rawFileLink: str | ||||
|   container.replaceChildren(elViewRawPrompt); | ||||
|  | ||||
|   if (errorMsg) { | ||||
|     const elErrorMessage = createElementFromHTML(htmlEscape`<div class="ui error message">${errorMsg}</div>`); | ||||
|     const elErrorMessage = createElementFromHTML(html`<div class="ui error message">${errorMsg}</div>`); | ||||
|     elViewRawPrompt.insertAdjacentElement('afterbegin', elErrorMessage); | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| import {htmlEscape} from 'escape-goat'; | ||||
| import {html, htmlRaw} from '../utils/html.ts'; | ||||
| import {createCodeEditor} from './codeeditor.ts'; | ||||
| import {hideElem, queryElems, showElem, createElementFromHTML} from '../utils/dom.ts'; | ||||
| import {attachRefIssueContextPopup} from './contextpopup.ts'; | ||||
| @@ -87,10 +87,10 @@ export function initRepoEditor() { | ||||
|         if (i < parts.length - 1) { | ||||
|           if (trimValue.length) { | ||||
|             const linkElement = createElementFromHTML( | ||||
|               `<span class="section"><a href="#">${htmlEscape(value)}</a></span>`, | ||||
|               html`<span class="section"><a href="#">${value}</a></span>`, | ||||
|             ); | ||||
|             const dividerElement = createElementFromHTML( | ||||
|               `<div class="breadcrumb-divider">/</div>`, | ||||
|               html`<div class="breadcrumb-divider">/</div>`, | ||||
|             ); | ||||
|             links.push(linkElement); | ||||
|             dividers.push(dividerElement); | ||||
| @@ -113,7 +113,7 @@ export function initRepoEditor() { | ||||
|       if (!warningDiv) { | ||||
|         warningDiv = document.createElement('div'); | ||||
|         warningDiv.classList.add('ui', 'warning', 'message', 'flash-message', 'flash-warning', 'space-related'); | ||||
|         warningDiv.innerHTML = '<p>File path contains leading or trailing whitespace.</p>'; | ||||
|         warningDiv.innerHTML = html`<p>File path contains leading or trailing whitespace.</p>`; | ||||
|         // Add display 'block' because display is set to 'none' in formantic\build\semantic.css | ||||
|         warningDiv.style.display = 'block'; | ||||
|         const inputContainer = document.querySelector('.repo-editor-header'); | ||||
| @@ -196,7 +196,8 @@ export function initRepoEditor() { | ||||
|   })(); | ||||
| } | ||||
|  | ||||
| export function renderPreviewPanelContent(previewPanel: Element, content: string) { | ||||
|   previewPanel.innerHTML = `<div class="render-content markup">${content}</div>`; | ||||
| export function renderPreviewPanelContent(previewPanel: Element, htmlContent: string) { | ||||
|   // the content is from the server, so it is safe to use innerHTML | ||||
|   previewPanel.innerHTML = html`<div class="render-content markup">${htmlRaw(htmlContent)}</div>`; | ||||
|   attachRefIssueContextPopup(previewPanel.querySelectorAll('p .ref-issue')); | ||||
| } | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| import {updateIssuesMeta} from './repo-common.ts'; | ||||
| import {toggleElem, queryElems, isElemVisible} from '../utils/dom.ts'; | ||||
| import {htmlEscape} from 'escape-goat'; | ||||
| import {html} from '../utils/html.ts'; | ||||
| import {confirmModal} from './comp/ConfirmModal.ts'; | ||||
| import {showErrorToast} from '../modules/toast.ts'; | ||||
| import {createSortable} from '../modules/sortable.ts'; | ||||
| @@ -138,10 +138,10 @@ function initDropdownUserRemoteSearch(el: Element) { | ||||
|         // the content is provided by backend IssuePosters handler | ||||
|         processedResults.length = 0; | ||||
|         for (const item of resp.results) { | ||||
|           let html = `<img class="ui avatar tw-align-middle" src="${htmlEscape(item.avatar_link)}" aria-hidden="true" alt width="20" height="20"><span class="gt-ellipsis">${htmlEscape(item.username)}</span>`; | ||||
|           if (item.full_name) html += `<span class="search-fullname tw-ml-2">${htmlEscape(item.full_name)}</span>`; | ||||
|           let nameHtml = html`<img class="ui avatar tw-align-middle" src="${item.avatar_link}" aria-hidden="true" alt width="20" height="20"><span class="gt-ellipsis">${item.username}</span>`; | ||||
|           if (item.full_name) nameHtml += html`<span class="search-fullname tw-ml-2">${item.full_name}</span>`; | ||||
|           if (selectedUsername.toLowerCase() === item.username.toLowerCase()) selectedUsername = item.username; | ||||
|           processedResults.push({value: item.username, name: html}); | ||||
|           processedResults.push({value: item.username, name: nameHtml}); | ||||
|         } | ||||
|         resp.results = processedResults; | ||||
|         return resp; | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| import {htmlEscape} from 'escape-goat'; | ||||
| import {html, htmlEscape} from '../utils/html.ts'; | ||||
| import {createTippy, showTemporaryTooltip} from '../modules/tippy.ts'; | ||||
| import { | ||||
|   addDelegatedEventListener, | ||||
| @@ -46,8 +46,7 @@ export function initRepoIssueSidebarDependency() { | ||||
|           if (String(issue.id) === currIssueId) continue; | ||||
|           filteredResponse.results.push({ | ||||
|             value: issue.id, | ||||
|             name: `<div class="gt-ellipsis">#${issue.number} ${htmlEscape(issue.title)}</div> | ||||
| <div class="text small tw-break-anywhere">${htmlEscape(issue.repository.full_name)}</div>`, | ||||
|             name: html`<div class="gt-ellipsis">#${issue.number} ${issue.title}</div><div class="text small tw-break-anywhere">${issue.repository.full_name}</div>`, | ||||
|           }); | ||||
|         } | ||||
|         return filteredResponse; | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| import {hideElem, querySingleVisibleElem, showElem, toggleElem} from '../utils/dom.ts'; | ||||
| import {htmlEscape} from 'escape-goat'; | ||||
| import {htmlEscape} from '../utils/html.ts'; | ||||
| import {fomanticQuery} from '../modules/fomantic/base.ts'; | ||||
| import {sanitizeRepoName} from './repo-common.ts'; | ||||
|  | ||||
|   | ||||
| @@ -2,6 +2,7 @@ import {validateTextareaNonEmpty, initComboMarkdownEditor} from './comp/ComboMar | ||||
| import {fomanticMobileScreen} from '../modules/fomantic.ts'; | ||||
| import {POST} from '../modules/fetch.ts'; | ||||
| import type {ComboMarkdownEditor} from './comp/ComboMarkdownEditor.ts'; | ||||
| import {html, htmlRaw} from '../utils/html.ts'; | ||||
|  | ||||
| async function initRepoWikiFormEditor() { | ||||
|   const editArea = document.querySelector<HTMLTextAreaElement>('.repository.wiki .combo-markdown-editor textarea'); | ||||
| @@ -30,7 +31,7 @@ async function initRepoWikiFormEditor() { | ||||
|         const response = await POST(editor.previewUrl, {data: formData}); | ||||
|         const data = await response.text(); | ||||
|         lastContent = newContent; | ||||
|         previewTarget.innerHTML = `<div class="render-content markup ui segment">${data}</div>`; | ||||
|         previewTarget.innerHTML = html`<div class="render-content markup ui segment">${htmlRaw(data)}</div>`; | ||||
|       } catch (error) { | ||||
|         console.error('Error rendering preview:', error); | ||||
|       } finally { | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| import {emojiKeys, emojiHTML, emojiString} from './emoji.ts'; | ||||
| import {htmlEscape} from 'escape-goat'; | ||||
| import {html, htmlRaw} from '../utils/html.ts'; | ||||
|  | ||||
| type TributeItem = Record<string, any>; | ||||
|  | ||||
| @@ -26,17 +26,18 @@ export async function attachTribute(element: HTMLElement) { | ||||
|         return emojiString(item.original); | ||||
|       }, | ||||
|       menuItemTemplate: (item: TributeItem) => { | ||||
|         return `<div class="tribute-item">${emojiHTML(item.original)}<span>${htmlEscape(item.original)}</span></div>`; | ||||
|         return html`<div class="tribute-item">${htmlRaw(emojiHTML(item.original))}<span>${item.original}</span></div>`; | ||||
|       }, | ||||
|     }, { // mentions | ||||
|       values: window.config.mentionValues ?? [], | ||||
|       requireLeadingSpace: true, | ||||
|       menuItemTemplate: (item: TributeItem) => { | ||||
|         return ` | ||||
|         const fullNameHtml = item.original.fullname && item.original.fullname !== '' ? html`<span class="fullname">${item.original.fullname}</span>` : ''; | ||||
|         return html` | ||||
|           <div class="tribute-item"> | ||||
|             <img alt src="${htmlEscape(item.original.avatar)}" width="21" height="21"/> | ||||
|             <span class="name">${htmlEscape(item.original.name)}</span> | ||||
|             ${item.original.fullname && item.original.fullname !== '' ? `<span class="fullname">${htmlEscape(item.original.fullname)}</span>` : ''} | ||||
|             <img alt src="${item.original.avatar}" width="21" height="21"/> | ||||
|             <span class="name">${item.original.name}</span> | ||||
|             ${htmlRaw(fullNameHtml)} | ||||
|           </div> | ||||
|         `; | ||||
|       }, | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| import {htmlEscape} from 'escape-goat'; | ||||
| import {html, htmlRaw} from '../utils/html.ts'; | ||||
|  | ||||
| type Processor = (el: HTMLElement) => string | HTMLElement | void; | ||||
|  | ||||
| @@ -38,10 +38,10 @@ function prepareProcessors(ctx:ProcessorContext): Processors { | ||||
|     IMG(el: HTMLElement) { | ||||
|       const alt = el.getAttribute('alt') || 'image'; | ||||
|       const src = el.getAttribute('src'); | ||||
|       const widthAttr = el.hasAttribute('width') ? ` width="${htmlEscape(el.getAttribute('width') || '')}"` : ''; | ||||
|       const heightAttr = el.hasAttribute('height') ? ` height="${htmlEscape(el.getAttribute('height') || '')}"` : ''; | ||||
|       const widthAttr = el.hasAttribute('width') ? htmlRaw` width="${el.getAttribute('width') || ''}"` : ''; | ||||
|       const heightAttr = el.hasAttribute('height') ? htmlRaw` height="${el.getAttribute('height') || ''}"` : ''; | ||||
|       if (widthAttr || heightAttr) { | ||||
|         return `<img alt="${htmlEscape(alt)}"${widthAttr}${heightAttr} src="${htmlEscape(src)}">`; | ||||
|         return html`<img alt="${alt}"${widthAttr}${heightAttr} src="${src}">`; | ||||
|       } | ||||
|       return ``; | ||||
|     }, | ||||
|   | ||||
| @@ -2,6 +2,7 @@ import {isDarkTheme} from '../utils.ts'; | ||||
| import {makeCodeCopyButton} from './codecopy.ts'; | ||||
| import {displayError} from './common.ts'; | ||||
| import {queryElems} from '../utils/dom.ts'; | ||||
| import {html, htmlRaw} from '../utils/html.ts'; | ||||
|  | ||||
| const {mermaidMaxSourceCharacters} = window.config; | ||||
|  | ||||
| @@ -46,7 +47,7 @@ export async function initMarkupCodeMermaid(elMarkup: HTMLElement): Promise<void | ||||
|  | ||||
|       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>`; | ||||
|       iframe.srcdoc = html`<html><head><style>${htmlRaw(iframeCss)}</style></head><body>${htmlRaw(svg)}</body></html>`; | ||||
|  | ||||
|       const mermaidBlock = document.createElement('div'); | ||||
|       mermaidBlock.classList.add('mermaid-block', 'is-loading', 'tw-hidden'); | ||||
|   | ||||
| @@ -2,6 +2,7 @@ import tippy, {followCursor} from 'tippy.js'; | ||||
| import {isDocumentFragmentOrElementNode} from '../utils/dom.ts'; | ||||
| import {formatDatetime} from '../utils/time.ts'; | ||||
| import type {Content, Instance, Placement, Props} from 'tippy.js'; | ||||
| import {html} from '../utils/html.ts'; | ||||
|  | ||||
| type TippyOpts = { | ||||
|   role?: string, | ||||
| @@ -9,7 +10,7 @@ type TippyOpts = { | ||||
| } & Partial<Props>; | ||||
|  | ||||
| const visibleInstances = new Set<Instance>(); | ||||
| const arrowSvg = `<svg width="16" height="7"><path d="m0 7 8-7 8 7Z" class="tippy-svg-arrow-outer"/><path d="m0 8 8-7 8 7Z" class="tippy-svg-arrow-inner"/></svg>`; | ||||
| const arrowSvg = html`<svg width="16" height="7"><path d="m0 7 8-7 8 7Z" class="tippy-svg-arrow-outer"/><path d="m0 8 8-7 8 7Z" class="tippy-svg-arrow-inner"/></svg>`; | ||||
|  | ||||
| export function createTippy(target: Element, opts: TippyOpts = {}): Instance { | ||||
|   // the callback functions should be destructured from opts, | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| import {htmlEscape} from 'escape-goat'; | ||||
| import {htmlEscape} from '../utils/html.ts'; | ||||
| import {svg} from '../svg.ts'; | ||||
| import {animateOnce, queryElems, showElem} from '../utils/dom.ts'; | ||||
| import Toastify from 'toastify-js'; // don't use "async import", because when network error occurs, the "async import" also fails and nothing is shown | ||||
|   | ||||
| @@ -1,5 +1,6 @@ | ||||
| import {defineComponent, h, type PropType} from 'vue'; | ||||
| import {parseDom, serializeXml} from './utils.ts'; | ||||
| import {html, htmlRaw} from './utils/html.ts'; | ||||
| import giteaDoubleChevronLeft from '../../public/assets/img/svg/gitea-double-chevron-left.svg'; | ||||
| import giteaDoubleChevronRight from '../../public/assets/img/svg/gitea-double-chevron-right.svg'; | ||||
| import giteaEmptyCheckbox from '../../public/assets/img/svg/gitea-empty-checkbox.svg'; | ||||
| @@ -220,7 +221,7 @@ export const SvgIcon = defineComponent({ | ||||
|     const classes = Array.from(svgOuter.classList); | ||||
|     if (this.symbolId) { | ||||
|       classes.push('tw-hidden', 'svg-symbol-container'); | ||||
|       svgInnerHtml = `<symbol id="${this.symbolId}" viewBox="${attrs['^viewBox']}">${svgInnerHtml}</symbol>`; | ||||
|       svgInnerHtml = html`<symbol id="${this.symbolId}" viewBox="${attrs['^viewBox']}">${htmlRaw(svgInnerHtml)}</symbol>`; | ||||
|     } | ||||
|     // create VNode | ||||
|     return h('svg', { | ||||
|   | ||||
| @@ -314,6 +314,7 @@ export function replaceTextareaSelection(textarea: HTMLTextAreaElement, text: st | ||||
| export function createElementFromHTML<T extends HTMLElement>(htmlString: string): T { | ||||
|   htmlString = htmlString.trim(); | ||||
|   // some tags like "tr" are special, it must use a correct parent container to create | ||||
|   // eslint-disable-next-line github/unescaped-html-literal -- FIXME: maybe we need to use other approaches to create elements from HTML, e.g. using DOMParser | ||||
|   if (htmlString.startsWith('<tr')) { | ||||
|     const container = document.createElement('table'); | ||||
|     container.innerHTML = htmlString; | ||||
|   | ||||
							
								
								
									
										8
									
								
								web_src/js/utils/html.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								web_src/js/utils/html.test.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,8 @@ | ||||
| import {html, htmlEscape, htmlRaw} from './html.ts'; | ||||
|  | ||||
| test('html', async () => { | ||||
|   expect(html`<a>${'<>&\'"'}</a>`).toBe(`<a><>&'"</a>`); | ||||
|   expect(html`<a>${htmlRaw('<img>')}</a>`).toBe(`<a><img></a>`); | ||||
|   expect(html`<a>${htmlRaw`<img ${'&'}>`}</a>`).toBe(`<a><img &></a>`); | ||||
|   expect(htmlEscape(`<a></a>`)).toBe(`<a></a>`); | ||||
| }); | ||||
							
								
								
									
										32
									
								
								web_src/js/utils/html.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								web_src/js/utils/html.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,32 @@ | ||||
| export function htmlEscape(s: string, ...args: Array<any>): string { | ||||
|   if (args.length !== 0) throw new Error('use html or htmlRaw instead of htmlEscape'); // check legacy usages | ||||
|   return s.replace(/&/g, '&') | ||||
|     .replace(/"/g, '"') | ||||
|     .replace(/'/g, ''') | ||||
|     .replace(/</g, '<') | ||||
|     .replace(/>/g, '>'); | ||||
| } | ||||
|  | ||||
| class rawObject { | ||||
|   private readonly value: string; | ||||
|   constructor(v: string) { this.value = v } | ||||
|   toString(): string { return this.value } | ||||
| } | ||||
|  | ||||
| export function html(tmpl: TemplateStringsArray, ...parts: Array<any>): string { | ||||
|   let output = tmpl[0]; | ||||
|   for (let i = 0; i < parts.length; i++) { | ||||
|     const value = parts[i]; | ||||
|     const valueEscaped = (value instanceof rawObject) ? value.toString() : htmlEscape(String(parts[i])); | ||||
|     output = output + valueEscaped + tmpl[i + 1]; | ||||
|   } | ||||
|   return output; | ||||
| } | ||||
|  | ||||
| export function htmlRaw(s: string|TemplateStringsArray, ...tmplParts: Array<any>): rawObject { | ||||
|   if (typeof s === 'string') { | ||||
|     if (tmplParts.length !== 0) throw new Error("either htmlRaw('str') or htmlRaw`tmpl`"); | ||||
|     return new rawObject(s); | ||||
|   } | ||||
|   return new rawObject(html(s, ...tmplParts)); | ||||
| } | ||||
		Reference in New Issue
	
	Block a user