mirror of
https://github.com/go-gitea/gitea.git
synced 2025-07-04 00:01:16 -04:00
Compare commits
No commits in common. "1d26694e1799d323c3cca91af50c6b6937d90b10" and "127274defc24a2ce6b550744ffcbe249f0b01e0e" have entirely different histories.
1d26694e17
...
127274defc
@ -91,7 +91,6 @@ 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],
|
||||
@ -424,7 +423,7 @@ module.exports = {
|
||||
'github/no-useless-passive': [2],
|
||||
'github/prefer-observers': [2],
|
||||
'github/require-passive-events': [2],
|
||||
'github/unescaped-html-literal': [2],
|
||||
'github/unescaped-html-literal': [0],
|
||||
'grouped-accessor-pairs': [2],
|
||||
'guard-for-in': [0],
|
||||
'id-blacklist': [0],
|
||||
|
@ -2769,8 +2769,6 @@ branch.new_branch_from = Create new branch from "%s"
|
||||
branch.renamed = Branch %s was renamed to %s.
|
||||
branch.rename_default_or_protected_branch_error = Only admins can rename default or protected branches.
|
||||
branch.rename_protected_branch_failed = This branch is protected by glob-based protection rules.
|
||||
branch.commits_divergence_from = Commits divergence: %[1]d behind and %[2]d ahead of %[3]s
|
||||
branch.commits_no_divergence = The same as branch %[1]s
|
||||
|
||||
tag.create_tag = Create tag %s
|
||||
tag.create_tag_operation = Create tag
|
||||
|
@ -2782,7 +2782,6 @@ topic.done=Déanta
|
||||
topic.count_prompt=Ní féidir leat níos mó ná 25 topaicí a roghnú
|
||||
topic.format_prompt=Ní mór do thopaicí tosú le litir nó uimhir, is féidir daiseanna ('-') agus poncanna ('.') a áireamh, a bheith suas le 35 carachtar ar fad. Ní mór litreacha a bheith i litreacha beaga.
|
||||
|
||||
find_file.follow_symlink=Lean an nasc siombalach seo go dtí an áit a bhfuil sé ag pointeáil air
|
||||
find_file.go_to_file=Téigh go dtí an comhad
|
||||
find_file.no_matching=Níl aon chomhad meaitseála le fáil
|
||||
|
||||
|
@ -1562,8 +1562,8 @@ issues.filter_project=Planeamento
|
||||
issues.filter_project_all=Todos os planeamentos
|
||||
issues.filter_project_none=Nenhum planeamento
|
||||
issues.filter_assignee=Encarregado
|
||||
issues.filter_assignee_no_assignee=Não atribuída
|
||||
issues.filter_assignee_any_assignee=Atribuída a alguém
|
||||
issues.filter_assignee_no_assignee=Não atribuído
|
||||
issues.filter_assignee_any_assignee=Atribuído a qualquer pessoa
|
||||
issues.filter_poster=Autor(a)
|
||||
issues.filter_user_placeholder=Procurar utilizadores
|
||||
issues.filter_user_no_select=Todos os utilizadores
|
||||
@ -1969,7 +1969,6 @@ pulls.cmd_instruction_checkout_title=Checkout
|
||||
pulls.cmd_instruction_checkout_desc=A partir do seu repositório, crie um novo ramo e teste nele as modificações.
|
||||
pulls.cmd_instruction_merge_title=Integrar
|
||||
pulls.cmd_instruction_merge_desc=Integrar as modificações e enviar para o Gitea.
|
||||
pulls.cmd_instruction_merge_warning=Aviso: Esta operação não pode executar pedidos de integração porque a opção "auto-identificar integração manual" não está habilitada.
|
||||
pulls.clear_merge_message=Apagar mensagem de integração
|
||||
pulls.clear_merge_message_hint=Apagar a mensagem de integração apenas remove o conteúdo da mensagem de cometimento e mantém os rodapés do git, tais como "Co-Autorado-Por …".
|
||||
|
||||
@ -2782,7 +2781,6 @@ topic.done=Concluído
|
||||
topic.count_prompt=Não pode escolher mais do que 25 tópicos
|
||||
topic.format_prompt=Os tópicos devem começar com uma letra ou um número, podem incluir traços ('-') ou pontos ('.') e podem ter até 35 caracteres. As letras têm que ser minúsculas.
|
||||
|
||||
find_file.follow_symlink=Seguir esta ligação simbólica para onde ela está apontando
|
||||
find_file.go_to_file=Ir para o ficheiro
|
||||
find_file.no_matching=Não foi encontrado qualquer ficheiro correspondente
|
||||
|
||||
|
13
package-lock.json
generated
13
package-lock.json
generated
@ -28,6 +28,7 @@
|
||||
"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",
|
||||
@ -6562,6 +6563,18 @@
|
||||
"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,6 +27,7 @@
|
||||
"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",
|
||||
|
@ -443,10 +443,6 @@ func ViewPullMergeBox(ctx *context.Context) {
|
||||
preparePullViewPullInfo(ctx, issue)
|
||||
preparePullViewReviewAndMerge(ctx, issue)
|
||||
ctx.Data["PullMergeBoxReloading"] = issue.PullRequest.IsChecking()
|
||||
|
||||
// TODO: it should use a dedicated struct to render the pull merge box, to make sure all data is prepared correctly
|
||||
ctx.Data["IsIssuePoster"] = ctx.IsSigned && issue.IsPoster(ctx.Doer.ID)
|
||||
ctx.Data["HasIssuesOrPullsWritePermission"] = ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull)
|
||||
ctx.HTML(http.StatusOK, tplPullMergeBox)
|
||||
}
|
||||
|
||||
|
@ -107,14 +107,8 @@
|
||||
{{end}}
|
||||
</td>
|
||||
<td class="two wide ui">
|
||||
{{if and (not .DBBranch.IsDeleted) $.DefaultBranchBranch}}
|
||||
{{$tooltipDivergence := ""}}
|
||||
{{if or .CommitsBehind .CommitsAhead}}
|
||||
{{$tooltipDivergence = ctx.Locale.Tr "repo.branch.commits_divergence_from" .CommitsBehind .CommitsAhead $.DefaultBranchBranch.DBBranch.Name}}
|
||||
{{else}}
|
||||
{{$tooltipDivergence = ctx.Locale.Tr "repo.branch.commits_no_divergence" $.DefaultBranchBranch.DBBranch.Name}}
|
||||
{{end}}
|
||||
<div class="commit-divergence" data-tooltip-content="{{$tooltipDivergence}}">
|
||||
{{if and (not .DBBranch.IsDeleted) $.DefaultBranchBranch}}
|
||||
<div class="commit-divergence">
|
||||
<div class="bar-group">
|
||||
<div class="count count-behind">{{.CommitsBehind}}</div>
|
||||
{{/* old code bears 0/0.0 = NaN output, so it might output invalid "width: NaNpx", it just works and doesn't caues any problem. */}}
|
||||
@ -125,7 +119,7 @@
|
||||
<div class="bar bar-ahead" style="width: {{Eval 100 "*" .CommitsAhead "/" "(" .CommitsBehind "+" .CommitsAhead "+" 0.0 ")"}}%"></div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
{{end}}
|
||||
</td>
|
||||
<td class="two wide tw-text-right">
|
||||
{{if not .LatestPullRequest}}
|
||||
|
@ -1,5 +1,5 @@
|
||||
{{if and (or .HasIssuesOrPullsWritePermission .IsIssuePoster) (not .HasMerged) (not .Issue.IsClosed) (not .IsPullWorkInProgress)}}
|
||||
<a data-global-init="initPullRequestWipToggle" data-title="{{.Issue.Title}}" data-wip-prefix="{{index .PullRequestWorkInProgressPrefixes 0}}" data-update-url="{{.Issue.Link}}/title">
|
||||
<a class="toggle-wip tw-block tw-mt-2" data-title="{{.Issue.Title}}" data-wip-prefix="{{index .PullRequestWorkInProgressPrefixes 0}}" data-update-url="{{.Issue.Link}}/title">
|
||||
{{ctx.Locale.Tr "repo.pulls.still_in_progress"}} {{ctx.Locale.Tr "repo.pulls.add_prefix" (index .PullRequestWorkInProgressPrefixes 0)}}
|
||||
</a>
|
||||
{{end}}
|
||||
|
@ -95,7 +95,7 @@
|
||||
{{ctx.Locale.Tr "repo.pulls.cannot_merge_work_in_progress"}}
|
||||
</div>
|
||||
{{if or .HasIssuesOrPullsWritePermission .IsIssuePoster}}
|
||||
<button class="ui compact button" data-global-init="initPullRequestWipToggle" data-title="{{.Issue.Title}}" data-wip-prefix="{{.WorkInProgressPrefix}}" data-update-url="{{.Issue.Link}}/title">
|
||||
<button class="ui compact button toggle-wip" data-title="{{.Issue.Title}}" data-wip-prefix="{{.WorkInProgressPrefix}}" data-update-url="{{.Issue.Link}}/title">
|
||||
{{ctx.Locale.Tr "repo.pulls.remove_prefix" .WorkInProgressPrefix}}
|
||||
</button>
|
||||
{{end}}
|
||||
|
@ -2,7 +2,6 @@
|
||||
// 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.
|
||||
@ -24,7 +23,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 = 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>`;
|
||||
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>`;
|
||||
msgDiv = el.childNodes[0] as HTMLDivElement;
|
||||
}
|
||||
// merge duplicated messages into "the message (count)" format
|
||||
|
@ -2,7 +2,6 @@ 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({
|
||||
@ -17,7 +16,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(html`<div class="global-svg-icon-pool tw-hidden"></div>`);
|
||||
const svgContainer = createElementFromHTML('<div class="global-svg-icon-pool tw-hidden"></div>');
|
||||
svgContainer.innerHTML = poolSvgs.join('');
|
||||
document.body.append(svgContainer);
|
||||
}
|
||||
|
@ -43,16 +43,13 @@ export function initGlobalDeleteButton(): void {
|
||||
|
||||
fomanticQuery(modal).modal({
|
||||
closable: false,
|
||||
onApprove: () => {
|
||||
onApprove: async () => {
|
||||
// if `data-type="form"` exists, then submit the form by the selector provided by `data-form="..."`
|
||||
if (btn.getAttribute('data-type') === 'form') {
|
||||
const formSelector = btn.getAttribute('data-form');
|
||||
const form = document.querySelector<HTMLFormElement>(formSelector);
|
||||
if (!form) throw new Error(`no form named ${formSelector} found`);
|
||||
modal.classList.add('is-loading'); // the form is not in the modal, so also add loading indicator to the modal
|
||||
form.classList.add('is-loading');
|
||||
form.submit();
|
||||
return false; // prevent modal from closing automatically
|
||||
}
|
||||
|
||||
// prepare an AJAX form by data attributes
|
||||
@ -65,15 +62,12 @@ export function initGlobalDeleteButton(): void {
|
||||
postData.append('id', value);
|
||||
}
|
||||
}
|
||||
(async () => {
|
||||
const response = await POST(btn.getAttribute('data-url'), {data: postData});
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
window.location.href = data.redirect;
|
||||
}
|
||||
})();
|
||||
modal.classList.add('is-loading'); // the request is in progress, so also add loading indicator to the modal
|
||||
return false; // prevent modal from closing automatically
|
||||
|
||||
const response = await POST(btn.getAttribute('data-url'), {data: postData});
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
window.location.href = data.redirect;
|
||||
}
|
||||
},
|
||||
}).modal('show');
|
||||
});
|
||||
@ -164,7 +158,13 @@ function onShowModalClick(el: HTMLElement, e: MouseEvent) {
|
||||
}
|
||||
}
|
||||
|
||||
fomanticQuery(elModal).modal('show');
|
||||
fomanticQuery(elModal).modal('setting', {
|
||||
onApprove: () => {
|
||||
// "form-fetch-action" can handle network errors gracefully,
|
||||
// so keep the modal dialog to make users can re-submit the form if anything wrong happens.
|
||||
if (elModal.querySelector('.form-fetch-action')) return false;
|
||||
},
|
||||
}).modal('show');
|
||||
}
|
||||
|
||||
export function initGlobalButtons(): void {
|
||||
|
@ -1,5 +1,5 @@
|
||||
import {svg} from '../../svg.ts';
|
||||
import {html, htmlRaw} from '../../utils/html.ts';
|
||||
import {htmlEscape} from 'escape-goat';
|
||||
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 ? 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());
|
||||
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>
|
||||
`);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
|
@ -72,7 +72,6 @@ export function initCompLabelEdit(pageSelector: string) {
|
||||
return false;
|
||||
}
|
||||
submitFormFetchAction(form);
|
||||
return false;
|
||||
},
|
||||
}).modal('show');
|
||||
};
|
||||
|
@ -1,4 +1,4 @@
|
||||
import {htmlEscape} from '../../utils/html.ts';
|
||||
import {htmlEscape} from 'escape-goat';
|
||||
import {fomanticQuery} from '../../modules/fomantic/base.ts';
|
||||
|
||||
const {appSubUrl} = window.config;
|
||||
|
@ -1,5 +1,5 @@
|
||||
import {svg} from '../svg.ts';
|
||||
import {html} from '../utils/html.ts';
|
||||
import {htmlEscape} from 'escape-goat';
|
||||
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 = html`<img width="${Math.round(width / dppx)}" alt="${file.name}" src="attachments/${file.uuid}">`;
|
||||
fileMarkdown = `<img width="${Math.round(width / dppx)}" alt="${htmlEscape(file.name)}" src="attachments/${htmlEscape(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 = html`<video src="attachments/${file.uuid}" title="${file.name}" controls></video>`;
|
||||
fileMarkdown = `<video src="attachments/${htmlEscape(file.uuid)}" title="${htmlEscape(file.name)}" controls></video>`;
|
||||
}
|
||||
return fileMarkdown;
|
||||
}
|
||||
|
@ -1,5 +1,4 @@
|
||||
import emojis from '../../../assets/emoji.json' with {type: 'json'};
|
||||
import {html} from '../utils/html.ts';
|
||||
|
||||
const {assetUrlPrefix, customEmojis} = window.config;
|
||||
|
||||
@ -25,11 +24,12 @@ for (const key of emojiKeys) {
|
||||
export function emojiHTML(name: string) {
|
||||
let inner;
|
||||
if (Object.hasOwn(customEmojis, name)) {
|
||||
inner = html`<img alt=":${name}:" src="${assetUrlPrefix}/img/emoji/${name}.png">`;
|
||||
inner = `<img alt=":${name}:" src="${assetUrlPrefix}/img/emoji/${name}.png">`;
|
||||
} else {
|
||||
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
|
||||
|
@ -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 {html} from '../utils/html.ts';
|
||||
import {htmlEscape} from 'escape-goat';
|
||||
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(html`<div class="ui error message">${errorMsg}</div>`);
|
||||
const elErrorMessage = createElementFromHTML(htmlEscape`<div class="ui error message">${errorMsg}</div>`);
|
||||
elViewRawPrompt.insertAdjacentElement('afterbegin', elErrorMessage);
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import {html, htmlRaw} from '../utils/html.ts';
|
||||
import {htmlEscape} from 'escape-goat';
|
||||
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(
|
||||
html`<span class="section"><a href="#">${value}</a></span>`,
|
||||
`<span class="section"><a href="#">${htmlEscape(value)}</a></span>`,
|
||||
);
|
||||
const dividerElement = createElementFromHTML(
|
||||
html`<div class="breadcrumb-divider">/</div>`,
|
||||
`<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 = html`<p>File path contains leading or trailing whitespace.</p>`;
|
||||
warningDiv.innerHTML = '<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,8 +196,7 @@ export function initRepoEditor() {
|
||||
})();
|
||||
}
|
||||
|
||||
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>`;
|
||||
export function renderPreviewPanelContent(previewPanel: Element, content: string) {
|
||||
previewPanel.innerHTML = `<div class="render-content markup">${content}</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 {html} from '../utils/html.ts';
|
||||
import {htmlEscape} from 'escape-goat';
|
||||
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 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>`;
|
||||
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>`;
|
||||
if (selectedUsername.toLowerCase() === item.username.toLowerCase()) selectedUsername = item.username;
|
||||
processedResults.push({value: item.username, name: nameHtml});
|
||||
processedResults.push({value: item.username, name: html});
|
||||
}
|
||||
resp.results = processedResults;
|
||||
return resp;
|
||||
|
@ -1,4 +1,4 @@
|
||||
import {html, htmlEscape} from '../utils/html.ts';
|
||||
import {htmlEscape} from 'escape-goat';
|
||||
import {createTippy, showTemporaryTooltip} from '../modules/tippy.ts';
|
||||
import {
|
||||
addDelegatedEventListener,
|
||||
@ -17,7 +17,6 @@ import {showErrorToast} from '../modules/toast.ts';
|
||||
import {initRepoIssueSidebar} from './repo-issue-sidebar.ts';
|
||||
import {fomanticQuery} from '../modules/fomantic/base.ts';
|
||||
import {ignoreAreYouSure} from '../vendor/jquery.are-you-sure.ts';
|
||||
import {registerGlobalInitFunc} from '../modules/observer.ts';
|
||||
|
||||
const {appSubUrl} = window.config;
|
||||
|
||||
@ -46,7 +45,8 @@ export function initRepoIssueSidebarDependency() {
|
||||
if (String(issue.id) === currIssueId) continue;
|
||||
filteredResponse.results.push({
|
||||
value: issue.id,
|
||||
name: html`<div class="gt-ellipsis">#${issue.number} ${issue.title}</div><div class="text small tw-break-anywhere">${issue.repository.full_name}</div>`,
|
||||
name: `<div class="gt-ellipsis">#${issue.number} ${htmlEscape(issue.title)}</div>
|
||||
<div class="text small tw-break-anywhere">${htmlEscape(issue.repository.full_name)}</div>`,
|
||||
});
|
||||
}
|
||||
return filteredResponse;
|
||||
@ -416,20 +416,25 @@ export function initRepoIssueWipNewTitle() {
|
||||
|
||||
export function initRepoIssueWipToggle() {
|
||||
// Toggle WIP for existing PR
|
||||
registerGlobalInitFunc('initPullRequestWipToggle', (toggleWip) => toggleWip.addEventListener('click', async (e) => {
|
||||
queryElems(document, '.toggle-wip', (el) => el.addEventListener('click', async (e) => {
|
||||
e.preventDefault();
|
||||
const toggleWip = el;
|
||||
const title = toggleWip.getAttribute('data-title');
|
||||
const wipPrefix = toggleWip.getAttribute('data-wip-prefix');
|
||||
const updateUrl = toggleWip.getAttribute('data-update-url');
|
||||
|
||||
const params = new URLSearchParams();
|
||||
params.append('title', title?.startsWith(wipPrefix) ? title.slice(wipPrefix.length).trim() : `${wipPrefix.trim()} ${title}`);
|
||||
const response = await POST(updateUrl, {data: params});
|
||||
if (!response.ok) {
|
||||
showErrorToast(`Failed to toggle 'work in progress' status`);
|
||||
return;
|
||||
try {
|
||||
const params = new URLSearchParams();
|
||||
params.append('title', title?.startsWith(wipPrefix) ? title.slice(wipPrefix.length).trim() : `${wipPrefix.trim()} ${title}`);
|
||||
|
||||
const response = await POST(updateUrl, {data: params});
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to toggle WIP status');
|
||||
}
|
||||
window.location.reload();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
window.location.reload();
|
||||
}));
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
import {hideElem, querySingleVisibleElem, showElem, toggleElem} from '../utils/dom.ts';
|
||||
import {htmlEscape} from '../utils/html.ts';
|
||||
import {htmlEscape} from 'escape-goat';
|
||||
import {fomanticQuery} from '../modules/fomantic/base.ts';
|
||||
import {sanitizeRepoName} from './repo-common.ts';
|
||||
|
||||
|
@ -2,7 +2,6 @@ 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');
|
||||
@ -31,7 +30,7 @@ async function initRepoWikiFormEditor() {
|
||||
const response = await POST(editor.previewUrl, {data: formData});
|
||||
const data = await response.text();
|
||||
lastContent = newContent;
|
||||
previewTarget.innerHTML = html`<div class="render-content markup ui segment">${htmlRaw(data)}</div>`;
|
||||
previewTarget.innerHTML = `<div class="render-content markup ui segment">${data}</div>`;
|
||||
} catch (error) {
|
||||
console.error('Error rendering preview:', error);
|
||||
} finally {
|
||||
|
@ -1,5 +1,5 @@
|
||||
import {emojiKeys, emojiHTML, emojiString} from './emoji.ts';
|
||||
import {html, htmlRaw} from '../utils/html.ts';
|
||||
import {htmlEscape} from 'escape-goat';
|
||||
|
||||
type TributeItem = Record<string, any>;
|
||||
|
||||
@ -26,18 +26,17 @@ export async function attachTribute(element: HTMLElement) {
|
||||
return emojiString(item.original);
|
||||
},
|
||||
menuItemTemplate: (item: TributeItem) => {
|
||||
return html`<div class="tribute-item">${htmlRaw(emojiHTML(item.original))}<span>${item.original}</span></div>`;
|
||||
return `<div class="tribute-item">${emojiHTML(item.original)}<span>${htmlEscape(item.original)}</span></div>`;
|
||||
},
|
||||
}, { // mentions
|
||||
values: window.config.mentionValues ?? [],
|
||||
requireLeadingSpace: true,
|
||||
menuItemTemplate: (item: TributeItem) => {
|
||||
const fullNameHtml = item.original.fullname && item.original.fullname !== '' ? html`<span class="fullname">${item.original.fullname}</span>` : '';
|
||||
return html`
|
||||
return `
|
||||
<div class="tribute-item">
|
||||
<img alt src="${item.original.avatar}" width="21" height="21"/>
|
||||
<span class="name">${item.original.name}</span>
|
||||
${htmlRaw(fullNameHtml)}
|
||||
<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>` : ''}
|
||||
</div>
|
||||
`;
|
||||
},
|
||||
|
@ -1,4 +1,4 @@
|
||||
import {html, htmlRaw} from '../utils/html.ts';
|
||||
import {htmlEscape} from 'escape-goat';
|
||||
|
||||
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') ? htmlRaw` width="${el.getAttribute('width') || ''}"` : '';
|
||||
const heightAttr = el.hasAttribute('height') ? htmlRaw` height="${el.getAttribute('height') || ''}"` : '';
|
||||
const widthAttr = el.hasAttribute('width') ? ` width="${htmlEscape(el.getAttribute('width') || '')}"` : '';
|
||||
const heightAttr = el.hasAttribute('height') ? ` height="${htmlEscape(el.getAttribute('height') || '')}"` : '';
|
||||
if (widthAttr || heightAttr) {
|
||||
return html`<img alt="${alt}"${widthAttr}${heightAttr} src="${src}">`;
|
||||
return `<img alt="${htmlEscape(alt)}"${widthAttr}${heightAttr} src="${htmlEscape(src)}">`;
|
||||
}
|
||||
return ``;
|
||||
},
|
||||
|
@ -2,7 +2,6 @@ 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;
|
||||
|
||||
@ -47,7 +46,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`<html><head><style>${htmlRaw(iframeCss)}</style></head><body>${htmlRaw(svg)}</body></html>`;
|
||||
iframe.srcdoc = `<html><head><style>${iframeCss}</style></head><body>${svg}</body></html>`;
|
||||
|
||||
const mermaidBlock = document.createElement('div');
|
||||
mermaidBlock.classList.add('mermaid-block', 'is-loading', 'tw-hidden');
|
||||
|
@ -9,9 +9,8 @@ const fomanticModalFn = $.fn.modal;
|
||||
export function initAriaModalPatch() {
|
||||
if ($.fn.modal === ariaModalFn) throw new Error('initAriaModalPatch could only be called once');
|
||||
$.fn.modal = ariaModalFn;
|
||||
(ariaModalFn as FomanticInitFunction).settings = fomanticModalFn.settings;
|
||||
$.fn.fomanticExt.onModalBeforeHidden = onModalBeforeHidden;
|
||||
$.fn.modal.settings.onApprove = onModalApproveDefault;
|
||||
(ariaModalFn as FomanticInitFunction).settings = fomanticModalFn.settings;
|
||||
}
|
||||
|
||||
// the patched `$.fn.modal` modal function
|
||||
@ -35,29 +34,6 @@ function ariaModalFn(this: any, ...args: Parameters<FomanticInitFunction>) {
|
||||
function onModalBeforeHidden(this: any) {
|
||||
const $modal = $(this);
|
||||
const elModal = $modal[0];
|
||||
queryElems(elModal, 'form', (form: HTMLFormElement) => form.reset());
|
||||
hideToastsFrom(elModal.closest('.ui.dimmer') ?? document.body);
|
||||
|
||||
// reset the form after the modal is hidden, after other modal events and handlers (e.g. "onApprove", form submit)
|
||||
setTimeout(() => {
|
||||
queryElems(elModal, 'form', (form: HTMLFormElement) => form.reset());
|
||||
}, 0);
|
||||
}
|
||||
|
||||
function onModalApproveDefault(this: any) {
|
||||
const $modal = $(this);
|
||||
const selectors = $modal.modal('setting', 'selector');
|
||||
const elModal = $modal[0];
|
||||
const elApprove = elModal.querySelector(selectors.approve);
|
||||
const elForm = elApprove?.closest('form');
|
||||
if (!elForm) return true; // no form, just allow closing the modal
|
||||
|
||||
// "form-fetch-action" can handle network errors gracefully,
|
||||
// so keep the modal dialog to make users can re-submit the form if anything wrong happens.
|
||||
if (elForm.matches('.form-fetch-action')) return false;
|
||||
|
||||
// There is an abuse for the "modal" + "form" combination, the "Approve" button is a traditional form submit button in the form.
|
||||
// Then "approve" and "submit" occur at the same time, the modal will be closed immediately before the form is submitted.
|
||||
// So here we prevent the modal from closing automatically by returning false, add the "is-loading" class to the form element.
|
||||
elForm.classList.add('is-loading');
|
||||
return false;
|
||||
}
|
||||
|
@ -2,7 +2,6 @@ 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,
|
||||
@ -10,7 +9,7 @@ type TippyOpts = {
|
||||
} & Partial<Props>;
|
||||
|
||||
const visibleInstances = new Set<Instance>();
|
||||
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>`;
|
||||
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>`;
|
||||
|
||||
export function createTippy(target: Element, opts: TippyOpts = {}): Instance {
|
||||
// the callback functions should be destructured from opts,
|
||||
|
@ -1,4 +1,4 @@
|
||||
import {htmlEscape} from '../utils/html.ts';
|
||||
import {htmlEscape} from 'escape-goat';
|
||||
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,6 +1,5 @@
|
||||
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';
|
||||
@ -221,7 +220,7 @@ export const SvgIcon = defineComponent({
|
||||
const classes = Array.from(svgOuter.classList);
|
||||
if (this.symbolId) {
|
||||
classes.push('tw-hidden', 'svg-symbol-container');
|
||||
svgInnerHtml = html`<symbol id="${this.symbolId}" viewBox="${attrs['^viewBox']}">${htmlRaw(svgInnerHtml)}</symbol>`;
|
||||
svgInnerHtml = `<symbol id="${this.symbolId}" viewBox="${attrs['^viewBox']}">${svgInnerHtml}</symbol>`;
|
||||
}
|
||||
// create VNode
|
||||
return h('svg', {
|
||||
|
@ -314,7 +314,6 @@ 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;
|
||||
|
@ -1,8 +0,0 @@
|
||||
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>`);
|
||||
});
|
@ -1,32 +0,0 @@
|
||||
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));
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user