mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-11-04 01:34:27 +00:00 
			
		
		
		
	@@ -91,6 +91,7 @@ module.exports = {
 | 
				
			|||||||
      plugins: ['@vitest/eslint-plugin'],
 | 
					      plugins: ['@vitest/eslint-plugin'],
 | 
				
			||||||
      globals: vitestPlugin.environments.env.globals,
 | 
					      globals: vitestPlugin.environments.env.globals,
 | 
				
			||||||
      rules: {
 | 
					      rules: {
 | 
				
			||||||
 | 
					        'github/unescaped-html-literal': [0],
 | 
				
			||||||
        '@vitest/consistent-test-filename': [0],
 | 
					        '@vitest/consistent-test-filename': [0],
 | 
				
			||||||
        '@vitest/consistent-test-it': [0],
 | 
					        '@vitest/consistent-test-it': [0],
 | 
				
			||||||
        '@vitest/expect-expect': [0],
 | 
					        '@vitest/expect-expect': [0],
 | 
				
			||||||
@@ -423,7 +424,7 @@ module.exports = {
 | 
				
			|||||||
    'github/no-useless-passive': [2],
 | 
					    'github/no-useless-passive': [2],
 | 
				
			||||||
    'github/prefer-observers': [2],
 | 
					    'github/prefer-observers': [2],
 | 
				
			||||||
    'github/require-passive-events': [2],
 | 
					    'github/require-passive-events': [2],
 | 
				
			||||||
    'github/unescaped-html-literal': [0],
 | 
					    'github/unescaped-html-literal': [2],
 | 
				
			||||||
    'grouped-accessor-pairs': [2],
 | 
					    'grouped-accessor-pairs': [2],
 | 
				
			||||||
    'guard-for-in': [0],
 | 
					    'guard-for-in': [0],
 | 
				
			||||||
    'id-blacklist': [0],
 | 
					    'id-blacklist': [0],
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										13
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										13
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							@@ -28,7 +28,6 @@
 | 
				
			|||||||
        "dropzone": "6.0.0-beta.2",
 | 
					        "dropzone": "6.0.0-beta.2",
 | 
				
			||||||
        "easymde": "2.20.0",
 | 
					        "easymde": "2.20.0",
 | 
				
			||||||
        "esbuild-loader": "4.3.0",
 | 
					        "esbuild-loader": "4.3.0",
 | 
				
			||||||
        "escape-goat": "4.0.0",
 | 
					 | 
				
			||||||
        "fast-glob": "3.3.3",
 | 
					        "fast-glob": "3.3.3",
 | 
				
			||||||
        "htmx.org": "2.0.6",
 | 
					        "htmx.org": "2.0.6",
 | 
				
			||||||
        "idiomorph": "0.7.3",
 | 
					        "idiomorph": "0.7.3",
 | 
				
			||||||
@@ -6563,18 +6562,6 @@
 | 
				
			|||||||
        "node": ">=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": {
 | 
					    "node_modules/escape-string-regexp": {
 | 
				
			||||||
      "version": "4.0.0",
 | 
					      "version": "4.0.0",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
 | 
					      "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",
 | 
					    "dropzone": "6.0.0-beta.2",
 | 
				
			||||||
    "easymde": "2.20.0",
 | 
					    "easymde": "2.20.0",
 | 
				
			||||||
    "esbuild-loader": "4.3.0",
 | 
					    "esbuild-loader": "4.3.0",
 | 
				
			||||||
    "escape-goat": "4.0.0",
 | 
					 | 
				
			||||||
    "fast-glob": "3.3.3",
 | 
					    "fast-glob": "3.3.3",
 | 
				
			||||||
    "htmx.org": "2.0.6",
 | 
					    "htmx.org": "2.0.6",
 | 
				
			||||||
    "idiomorph": "0.7.3",
 | 
					    "idiomorph": "0.7.3",
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,6 +2,7 @@
 | 
				
			|||||||
// to make sure the error handler always works, we should never import `window.config`, because
 | 
					// to make sure the error handler always works, we should never import `window.config`, because
 | 
				
			||||||
// some user's custom template breaks it.
 | 
					// some user's custom template breaks it.
 | 
				
			||||||
import type {Intent} from './types.ts';
 | 
					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 sets up the URL prefix used in webpack's chunk loading.
 | 
				
			||||||
// This file must be imported before any lazy-loading is being attempted.
 | 
					// 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}"]`);
 | 
					  let msgDiv = msgContainer.querySelector<HTMLDivElement>(`.js-global-error[data-global-error-msg-compact="${msgCompact}"]`);
 | 
				
			||||||
  if (!msgDiv) {
 | 
					  if (!msgDiv) {
 | 
				
			||||||
    const el = document.createElement('div');
 | 
					    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;
 | 
					    msgDiv = el.childNodes[0] as HTMLDivElement;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  // merge duplicated messages into "the message (count)" format
 | 
					  // merge duplicated messages into "the message (count)" format
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,6 +2,7 @@ import {reactive} from 'vue';
 | 
				
			|||||||
import {GET} from '../modules/fetch.ts';
 | 
					import {GET} from '../modules/fetch.ts';
 | 
				
			||||||
import {pathEscapeSegments} from '../utils/url.ts';
 | 
					import {pathEscapeSegments} from '../utils/url.ts';
 | 
				
			||||||
import {createElementFromHTML} from '../utils/dom.ts';
 | 
					import {createElementFromHTML} from '../utils/dom.ts';
 | 
				
			||||||
 | 
					import {html} from '../utils/html.ts';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function createViewFileTreeStore(props: { repoLink: string, treePath: string, currentRefNameSubURL: string}) {
 | 
					export function createViewFileTreeStore(props: { repoLink: string, treePath: string, currentRefNameSubURL: string}) {
 | 
				
			||||||
  const store = reactive({
 | 
					  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 (!document.querySelector(`.global-svg-icon-pool #${svgId}`)) poolSvgs.push(svgContent);
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      if (poolSvgs.length) {
 | 
					      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('');
 | 
					        svgContainer.innerHTML = poolSvgs.join('');
 | 
				
			||||||
        document.body.append(svgContainer);
 | 
					        document.body.append(svgContainer);
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,5 +1,5 @@
 | 
				
			|||||||
import {svg} from '../../svg.ts';
 | 
					import {svg} from '../../svg.ts';
 | 
				
			||||||
import {htmlEscape} from 'escape-goat';
 | 
					import {html, htmlRaw} from '../../utils/html.ts';
 | 
				
			||||||
import {createElementFromHTML} from '../../utils/dom.ts';
 | 
					import {createElementFromHTML} from '../../utils/dom.ts';
 | 
				
			||||||
import {fomanticQuery} from '../../modules/fomantic/base.ts';
 | 
					import {fomanticQuery} from '../../modules/fomantic/base.ts';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -12,17 +12,17 @@ type ConfirmModalOptions = {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function createConfirmModal({header = '', content = '', confirmButtonColor = 'primary'}:ConfirmModalOptions = {}): HTMLElement {
 | 
					export function createConfirmModal({header = '', content = '', confirmButtonColor = 'primary'}:ConfirmModalOptions = {}): HTMLElement {
 | 
				
			||||||
  const headerHtml = header ? `<div class="header">${htmlEscape(header)}</div>` : '';
 | 
					  const headerHtml = header ? html`<div class="header">${header}</div>` : '';
 | 
				
			||||||
  return createElementFromHTML(`
 | 
					  return createElementFromHTML(html`
 | 
				
			||||||
<div class="ui g-modal-confirm modal">
 | 
					    <div class="ui g-modal-confirm modal">
 | 
				
			||||||
  ${headerHtml}
 | 
					      ${htmlRaw(headerHtml)}
 | 
				
			||||||
  <div class="content">${htmlEscape(content)}</div>
 | 
					      <div class="content">${content}</div>
 | 
				
			||||||
      <div class="actions">
 | 
					      <div class="actions">
 | 
				
			||||||
    <button class="ui cancel button">${svg('octicon-x')} ${htmlEscape(i18n.modal_cancel)}</button>
 | 
					        <button class="ui cancel button">${htmlRaw(svg('octicon-x'))} ${i18n.modal_cancel}</button>
 | 
				
			||||||
    <button class="ui ${confirmButtonColor} ok button">${svg('octicon-check')} ${htmlEscape(i18n.modal_confirm)}</button>
 | 
					        <button class="ui ${confirmButtonColor} ok button">${htmlRaw(svg('octicon-check'))} ${i18n.modal_confirm}</button>
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
</div>
 | 
					    </div>
 | 
				
			||||||
`);
 | 
					  `.trim());
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function confirmModal(modal: HTMLElement | ConfirmModalOptions): Promise<boolean> {
 | 
					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) {
 | 
					export function removeAttachmentLinksFromMarkdown(text: string, fileUuid: string) {
 | 
				
			||||||
  text = text.replace(new RegExp(`!?\\[([^\\]]+)\\]\\(/?attachments/${fileUuid}\\)`, 'g'), '');
 | 
					  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;
 | 
					  return text;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
import {htmlEscape} from 'escape-goat';
 | 
					import {htmlEscape} from '../../utils/html.ts';
 | 
				
			||||||
import {fomanticQuery} from '../../modules/fomantic/base.ts';
 | 
					import {fomanticQuery} from '../../modules/fomantic/base.ts';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const {appSubUrl} = window.config;
 | 
					const {appSubUrl} = window.config;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,5 +1,5 @@
 | 
				
			|||||||
import {svg} from '../svg.ts';
 | 
					import {svg} from '../svg.ts';
 | 
				
			||||||
import {htmlEscape} from 'escape-goat';
 | 
					import {html} from '../utils/html.ts';
 | 
				
			||||||
import {clippie} from 'clippie';
 | 
					import {clippie} from 'clippie';
 | 
				
			||||||
import {showTemporaryTooltip} from '../modules/tippy.ts';
 | 
					import {showTemporaryTooltip} from '../modules/tippy.ts';
 | 
				
			||||||
import {GET, POST} from '../modules/fetch.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
 | 
					      // 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.
 | 
					      // 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}"
 | 
					      // 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 {
 | 
					    } else {
 | 
				
			||||||
      // Markdown always renders the image with a relative path, so the final URL is "/sub-path/owner/repo/attachments/{uuid}"
 | 
					      // 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"
 | 
					      // TODO: it should also use relative path for consistency, because absolute is ambiguous for "/sub-path/attachments" or "/attachments"
 | 
				
			||||||
      fileMarkdown = ``;
 | 
					      fileMarkdown = ``;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  } else if (isVideoFile(file)) {
 | 
					  } 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;
 | 
					  return fileMarkdown;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,5 @@
 | 
				
			|||||||
import emojis from '../../../assets/emoji.json' with {type: 'json'};
 | 
					import emojis from '../../../assets/emoji.json' with {type: 'json'};
 | 
				
			||||||
 | 
					import {html} from '../utils/html.ts';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const {assetUrlPrefix, customEmojis} = window.config;
 | 
					const {assetUrlPrefix, customEmojis} = window.config;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -24,12 +25,11 @@ for (const key of emojiKeys) {
 | 
				
			|||||||
export function emojiHTML(name: string) {
 | 
					export function emojiHTML(name: string) {
 | 
				
			||||||
  let inner;
 | 
					  let inner;
 | 
				
			||||||
  if (Object.hasOwn(customEmojis, name)) {
 | 
					  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 {
 | 
					  } else {
 | 
				
			||||||
    inner = emojiString(name);
 | 
					    inner = emojiString(name);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					  return html`<span class="emoji" title=":${name}:">${inner}</span>`;
 | 
				
			||||||
  return `<span class="emoji" title=":${name}:">${inner}</span>`;
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// retrieve string for given emoji name
 | 
					// 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 {newRenderPluginPdfViewer} from '../render/plugins/pdf-viewer.ts';
 | 
				
			||||||
import {registerGlobalInitFunc} from '../modules/observer.ts';
 | 
					import {registerGlobalInitFunc} from '../modules/observer.ts';
 | 
				
			||||||
import {createElementFromHTML, showElem, toggleClass} from '../utils/dom.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';
 | 
					import {basename} from '../utils.ts';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const plugins: FileRenderPlugin[] = [];
 | 
					const plugins: FileRenderPlugin[] = [];
 | 
				
			||||||
@@ -54,7 +54,7 @@ async function renderRawFileToContainer(container: HTMLElement, rawFileLink: str
 | 
				
			|||||||
  container.replaceChildren(elViewRawPrompt);
 | 
					  container.replaceChildren(elViewRawPrompt);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if (errorMsg) {
 | 
					  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);
 | 
					    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 {createCodeEditor} from './codeeditor.ts';
 | 
				
			||||||
import {hideElem, queryElems, showElem, createElementFromHTML} from '../utils/dom.ts';
 | 
					import {hideElem, queryElems, showElem, createElementFromHTML} from '../utils/dom.ts';
 | 
				
			||||||
import {attachRefIssueContextPopup} from './contextpopup.ts';
 | 
					import {attachRefIssueContextPopup} from './contextpopup.ts';
 | 
				
			||||||
@@ -87,10 +87,10 @@ export function initRepoEditor() {
 | 
				
			|||||||
        if (i < parts.length - 1) {
 | 
					        if (i < parts.length - 1) {
 | 
				
			||||||
          if (trimValue.length) {
 | 
					          if (trimValue.length) {
 | 
				
			||||||
            const linkElement = createElementFromHTML(
 | 
					            const linkElement = createElementFromHTML(
 | 
				
			||||||
              `<span class="section"><a href="#">${htmlEscape(value)}</a></span>`,
 | 
					              html`<span class="section"><a href="#">${value}</a></span>`,
 | 
				
			||||||
            );
 | 
					            );
 | 
				
			||||||
            const dividerElement = createElementFromHTML(
 | 
					            const dividerElement = createElementFromHTML(
 | 
				
			||||||
              `<div class="breadcrumb-divider">/</div>`,
 | 
					              html`<div class="breadcrumb-divider">/</div>`,
 | 
				
			||||||
            );
 | 
					            );
 | 
				
			||||||
            links.push(linkElement);
 | 
					            links.push(linkElement);
 | 
				
			||||||
            dividers.push(dividerElement);
 | 
					            dividers.push(dividerElement);
 | 
				
			||||||
@@ -113,7 +113,7 @@ export function initRepoEditor() {
 | 
				
			|||||||
      if (!warningDiv) {
 | 
					      if (!warningDiv) {
 | 
				
			||||||
        warningDiv = document.createElement('div');
 | 
					        warningDiv = document.createElement('div');
 | 
				
			||||||
        warningDiv.classList.add('ui', 'warning', 'message', 'flash-message', 'flash-warning', 'space-related');
 | 
					        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
 | 
					        // Add display 'block' because display is set to 'none' in formantic\build\semantic.css
 | 
				
			||||||
        warningDiv.style.display = 'block';
 | 
					        warningDiv.style.display = 'block';
 | 
				
			||||||
        const inputContainer = document.querySelector('.repo-editor-header');
 | 
					        const inputContainer = document.querySelector('.repo-editor-header');
 | 
				
			||||||
@@ -196,7 +196,8 @@ export function initRepoEditor() {
 | 
				
			|||||||
  })();
 | 
					  })();
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function renderPreviewPanelContent(previewPanel: Element, content: string) {
 | 
					export function renderPreviewPanelContent(previewPanel: Element, htmlContent: string) {
 | 
				
			||||||
  previewPanel.innerHTML = `<div class="render-content markup">${content}</div>`;
 | 
					  // 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'));
 | 
					  attachRefIssueContextPopup(previewPanel.querySelectorAll('p .ref-issue'));
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,6 +1,6 @@
 | 
				
			|||||||
import {updateIssuesMeta} from './repo-common.ts';
 | 
					import {updateIssuesMeta} from './repo-common.ts';
 | 
				
			||||||
import {toggleElem, queryElems, isElemVisible} from '../utils/dom.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 {confirmModal} from './comp/ConfirmModal.ts';
 | 
				
			||||||
import {showErrorToast} from '../modules/toast.ts';
 | 
					import {showErrorToast} from '../modules/toast.ts';
 | 
				
			||||||
import {createSortable} from '../modules/sortable.ts';
 | 
					import {createSortable} from '../modules/sortable.ts';
 | 
				
			||||||
@@ -138,10 +138,10 @@ function initDropdownUserRemoteSearch(el: Element) {
 | 
				
			|||||||
        // the content is provided by backend IssuePosters handler
 | 
					        // the content is provided by backend IssuePosters handler
 | 
				
			||||||
        processedResults.length = 0;
 | 
					        processedResults.length = 0;
 | 
				
			||||||
        for (const item of resp.results) {
 | 
					        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>`;
 | 
					          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) html += `<span class="search-fullname tw-ml-2">${htmlEscape(item.full_name)}</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;
 | 
					          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;
 | 
					        resp.results = processedResults;
 | 
				
			||||||
        return resp;
 | 
					        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 {createTippy, showTemporaryTooltip} from '../modules/tippy.ts';
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  addDelegatedEventListener,
 | 
					  addDelegatedEventListener,
 | 
				
			||||||
@@ -46,8 +46,7 @@ export function initRepoIssueSidebarDependency() {
 | 
				
			|||||||
          if (String(issue.id) === currIssueId) continue;
 | 
					          if (String(issue.id) === currIssueId) continue;
 | 
				
			||||||
          filteredResponse.results.push({
 | 
					          filteredResponse.results.push({
 | 
				
			||||||
            value: issue.id,
 | 
					            value: issue.id,
 | 
				
			||||||
            name: `<div class="gt-ellipsis">#${issue.number} ${htmlEscape(issue.title)}</div>
 | 
					            name: html`<div class="gt-ellipsis">#${issue.number} ${issue.title}</div><div class="text small tw-break-anywhere">${issue.repository.full_name}</div>`,
 | 
				
			||||||
<div class="text small tw-break-anywhere">${htmlEscape(issue.repository.full_name)}</div>`,
 | 
					 | 
				
			||||||
          });
 | 
					          });
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        return filteredResponse;
 | 
					        return filteredResponse;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,5 +1,5 @@
 | 
				
			|||||||
import {hideElem, querySingleVisibleElem, showElem, toggleElem} from '../utils/dom.ts';
 | 
					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 {fomanticQuery} from '../modules/fomantic/base.ts';
 | 
				
			||||||
import {sanitizeRepoName} from './repo-common.ts';
 | 
					import {sanitizeRepoName} from './repo-common.ts';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,6 +2,7 @@ import {validateTextareaNonEmpty, initComboMarkdownEditor} from './comp/ComboMar
 | 
				
			|||||||
import {fomanticMobileScreen} from '../modules/fomantic.ts';
 | 
					import {fomanticMobileScreen} from '../modules/fomantic.ts';
 | 
				
			||||||
import {POST} from '../modules/fetch.ts';
 | 
					import {POST} from '../modules/fetch.ts';
 | 
				
			||||||
import type {ComboMarkdownEditor} from './comp/ComboMarkdownEditor.ts';
 | 
					import type {ComboMarkdownEditor} from './comp/ComboMarkdownEditor.ts';
 | 
				
			||||||
 | 
					import {html, htmlRaw} from '../utils/html.ts';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function initRepoWikiFormEditor() {
 | 
					async function initRepoWikiFormEditor() {
 | 
				
			||||||
  const editArea = document.querySelector<HTMLTextAreaElement>('.repository.wiki .combo-markdown-editor textarea');
 | 
					  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 response = await POST(editor.previewUrl, {data: formData});
 | 
				
			||||||
        const data = await response.text();
 | 
					        const data = await response.text();
 | 
				
			||||||
        lastContent = newContent;
 | 
					        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) {
 | 
					      } catch (error) {
 | 
				
			||||||
        console.error('Error rendering preview:', error);
 | 
					        console.error('Error rendering preview:', error);
 | 
				
			||||||
      } finally {
 | 
					      } finally {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,5 +1,5 @@
 | 
				
			|||||||
import {emojiKeys, emojiHTML, emojiString} from './emoji.ts';
 | 
					import {emojiKeys, emojiHTML, emojiString} from './emoji.ts';
 | 
				
			||||||
import {htmlEscape} from 'escape-goat';
 | 
					import {html, htmlRaw} from '../utils/html.ts';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type TributeItem = Record<string, any>;
 | 
					type TributeItem = Record<string, any>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -26,17 +26,18 @@ export async function attachTribute(element: HTMLElement) {
 | 
				
			|||||||
        return emojiString(item.original);
 | 
					        return emojiString(item.original);
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      menuItemTemplate: (item: TributeItem) => {
 | 
					      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
 | 
					    }, { // mentions
 | 
				
			||||||
      values: window.config.mentionValues ?? [],
 | 
					      values: window.config.mentionValues ?? [],
 | 
				
			||||||
      requireLeadingSpace: true,
 | 
					      requireLeadingSpace: true,
 | 
				
			||||||
      menuItemTemplate: (item: TributeItem) => {
 | 
					      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">
 | 
					          <div class="tribute-item">
 | 
				
			||||||
            <img alt src="${htmlEscape(item.original.avatar)}" width="21" height="21"/>
 | 
					            <img alt src="${item.original.avatar}" width="21" height="21"/>
 | 
				
			||||||
            <span class="name">${htmlEscape(item.original.name)}</span>
 | 
					            <span class="name">${item.original.name}</span>
 | 
				
			||||||
            ${item.original.fullname && item.original.fullname !== '' ? `<span class="fullname">${htmlEscape(item.original.fullname)}</span>` : ''}
 | 
					            ${htmlRaw(fullNameHtml)}
 | 
				
			||||||
          </div>
 | 
					          </div>
 | 
				
			||||||
        `;
 | 
					        `;
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
import {htmlEscape} from 'escape-goat';
 | 
					import {html, htmlRaw} from '../utils/html.ts';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type Processor = (el: HTMLElement) => string | HTMLElement | void;
 | 
					type Processor = (el: HTMLElement) => string | HTMLElement | void;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -38,10 +38,10 @@ function prepareProcessors(ctx:ProcessorContext): Processors {
 | 
				
			|||||||
    IMG(el: HTMLElement) {
 | 
					    IMG(el: HTMLElement) {
 | 
				
			||||||
      const alt = el.getAttribute('alt') || 'image';
 | 
					      const alt = el.getAttribute('alt') || 'image';
 | 
				
			||||||
      const src = el.getAttribute('src');
 | 
					      const src = el.getAttribute('src');
 | 
				
			||||||
      const widthAttr = el.hasAttribute('width') ? ` width="${htmlEscape(el.getAttribute('width') || '')}"` : '';
 | 
					      const widthAttr = el.hasAttribute('width') ? htmlRaw` width="${el.getAttribute('width') || ''}"` : '';
 | 
				
			||||||
      const heightAttr = el.hasAttribute('height') ? ` height="${htmlEscape(el.getAttribute('height') || '')}"` : '';
 | 
					      const heightAttr = el.hasAttribute('height') ? htmlRaw` height="${el.getAttribute('height') || ''}"` : '';
 | 
				
			||||||
      if (widthAttr || heightAttr) {
 | 
					      if (widthAttr || heightAttr) {
 | 
				
			||||||
        return `<img alt="${htmlEscape(alt)}"${widthAttr}${heightAttr} src="${htmlEscape(src)}">`;
 | 
					        return html`<img alt="${alt}"${widthAttr}${heightAttr} src="${src}">`;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      return ``;
 | 
					      return ``;
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,6 +2,7 @@ import {isDarkTheme} from '../utils.ts';
 | 
				
			|||||||
import {makeCodeCopyButton} from './codecopy.ts';
 | 
					import {makeCodeCopyButton} from './codecopy.ts';
 | 
				
			||||||
import {displayError} from './common.ts';
 | 
					import {displayError} from './common.ts';
 | 
				
			||||||
import {queryElems} from '../utils/dom.ts';
 | 
					import {queryElems} from '../utils/dom.ts';
 | 
				
			||||||
 | 
					import {html, htmlRaw} from '../utils/html.ts';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const {mermaidMaxSourceCharacters} = window.config;
 | 
					const {mermaidMaxSourceCharacters} = window.config;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -46,7 +47,7 @@ export async function initMarkupCodeMermaid(elMarkup: HTMLElement): Promise<void
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
      const iframe = document.createElement('iframe');
 | 
					      const iframe = document.createElement('iframe');
 | 
				
			||||||
      iframe.classList.add('markup-content-iframe', 'tw-invisible');
 | 
					      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');
 | 
					      const mermaidBlock = document.createElement('div');
 | 
				
			||||||
      mermaidBlock.classList.add('mermaid-block', 'is-loading', 'tw-hidden');
 | 
					      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 {isDocumentFragmentOrElementNode} from '../utils/dom.ts';
 | 
				
			||||||
import {formatDatetime} from '../utils/time.ts';
 | 
					import {formatDatetime} from '../utils/time.ts';
 | 
				
			||||||
import type {Content, Instance, Placement, Props} from 'tippy.js';
 | 
					import type {Content, Instance, Placement, Props} from 'tippy.js';
 | 
				
			||||||
 | 
					import {html} from '../utils/html.ts';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type TippyOpts = {
 | 
					type TippyOpts = {
 | 
				
			||||||
  role?: string,
 | 
					  role?: string,
 | 
				
			||||||
@@ -9,7 +10,7 @@ type TippyOpts = {
 | 
				
			|||||||
} & Partial<Props>;
 | 
					} & Partial<Props>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const visibleInstances = new Set<Instance>();
 | 
					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 {
 | 
					export function createTippy(target: Element, opts: TippyOpts = {}): Instance {
 | 
				
			||||||
  // the callback functions should be destructured from opts,
 | 
					  // 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 {svg} from '../svg.ts';
 | 
				
			||||||
import {animateOnce, queryElems, showElem} from '../utils/dom.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
 | 
					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 {defineComponent, h, type PropType} from 'vue';
 | 
				
			||||||
import {parseDom, serializeXml} from './utils.ts';
 | 
					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 giteaDoubleChevronLeft from '../../public/assets/img/svg/gitea-double-chevron-left.svg';
 | 
				
			||||||
import giteaDoubleChevronRight from '../../public/assets/img/svg/gitea-double-chevron-right.svg';
 | 
					import giteaDoubleChevronRight from '../../public/assets/img/svg/gitea-double-chevron-right.svg';
 | 
				
			||||||
import giteaEmptyCheckbox from '../../public/assets/img/svg/gitea-empty-checkbox.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);
 | 
					    const classes = Array.from(svgOuter.classList);
 | 
				
			||||||
    if (this.symbolId) {
 | 
					    if (this.symbolId) {
 | 
				
			||||||
      classes.push('tw-hidden', 'svg-symbol-container');
 | 
					      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
 | 
					    // create VNode
 | 
				
			||||||
    return h('svg', {
 | 
					    return h('svg', {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -314,6 +314,7 @@ export function replaceTextareaSelection(textarea: HTMLTextAreaElement, text: st
 | 
				
			|||||||
export function createElementFromHTML<T extends HTMLElement>(htmlString: string): T {
 | 
					export function createElementFromHTML<T extends HTMLElement>(htmlString: string): T {
 | 
				
			||||||
  htmlString = htmlString.trim();
 | 
					  htmlString = htmlString.trim();
 | 
				
			||||||
  // some tags like "tr" are special, it must use a correct parent container to create
 | 
					  // 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')) {
 | 
					  if (htmlString.startsWith('<tr')) {
 | 
				
			||||||
    const container = document.createElement('table');
 | 
					    const container = document.createElement('table');
 | 
				
			||||||
    container.innerHTML = htmlString;
 | 
					    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