feat: update gitea vendor
runner nix smoke / nix label and flake smoke (push) Failing after 1m28s

This commit is contained in:
2026-09-26 21:18:24 +00:00
parent c439c1b948
commit d9b2a4e787
3538 changed files with 116131 additions and 44340 deletions
@@ -9,6 +9,8 @@ import {
test('createElementFromHTML', () => {
expect(createElementFromHTML('<a>foo<span>bar</span></a>').outerHTML).toEqual('<a>foo<span>bar</span></a>');
expect(createElementFromHTML('<tr data-x="1"><td>foo</td></tr>').outerHTML).toEqual('<tr data-x="1"><td>foo</td></tr>');
expect(createElementFromHTML('<TR data-x="1"><td>foo</td></TR>').outerHTML).toEqual('<tr data-x="1"><td>foo</td></tr>');
expect(createElementFromHTML('<trx></trx>').outerHTML).toEqual('<trx></trx>');
});
test('createElementFromAttrs', () => {
+9 -26
View File
@@ -9,8 +9,8 @@ type ElementsCallback<T extends Element> = (el: T) => Promisable<any>;
type ElementsCallbackWithArgs = (el: Element, ...args: any[]) => Promisable<any>;
function elementsCall(el: ElementArg, func: ElementsCallbackWithArgs, ...args: any[]): ArrayLikeIterable<Element> {
if (typeof el === 'string' || el instanceof String) {
el = document.querySelectorAll(el as string);
if (typeof el === 'string') {
el = document.querySelectorAll(el);
}
if (el instanceof Node) {
func(el, ...args);
@@ -257,28 +257,6 @@ export function loadElem(el: LoadableElement, src: string) {
});
}
// some browsers like PaleMoon don't have "SubmitEvent" support, so polyfill it by a tricky method: use the last clicked button as submitter
// it can't use other transparent polyfill patches because PaleMoon also doesn't support "addEventListener(capture)"
const needSubmitEventPolyfill = typeof SubmitEvent === 'undefined';
export function submitEventSubmitter(e: any) {
e = e.originalEvent ?? e; // if the event is wrapped by jQuery, use "originalEvent", otherwise, use the event itself
return needSubmitEventPolyfill ? (e.target._submitter || null) : e.submitter;
}
function submitEventPolyfillListener(e: Event) {
const form = (e.target as HTMLElement).closest('form');
if (!form) return;
form._submitter = (e.target as HTMLElement).closest('button:not([type]), button[type="submit"], input[type="submit"]');
}
export function initSubmitEventPolyfill() {
if (!needSubmitEventPolyfill) return;
console.warn(`This browser doesn't have "SubmitEvent" support, use a tricky method to polyfill`);
document.body.addEventListener('click', submitEventPolyfillListener);
document.body.addEventListener('focus', submitEventPolyfillListener);
}
export function isElemVisible(el: HTMLElement): boolean {
// Check if an element is visible, equivalent to jQuery's `:visible` pseudo.
// This function DOESN'T account for all possible visibility scenarios, its behavior is covered by the tests of "querySingleVisibleElem"
@@ -289,9 +267,14 @@ export function isElemVisible(el: HTMLElement): boolean {
export function createElementFromHTML<T extends Element>(htmlString: string): T {
htmlString = htmlString.trim();
const isLetter = (code: number) => (code >= 65 && code <= 90) || (code >= 97 && code <= 122);
const startsWithTag = (s: string, tag: string) => {
return s.startsWith('<') &&
s.substring(1, 1 + tag.length).toLowerCase() === tag.toLowerCase() &&
!isLetter(s[1 + tag.length].charCodeAt(0));
};
// There is no way to create some elements without a proper parent, jQuery's approach: https://github.com/jquery/jquery/blob/main/src/manipulation/wrapMap.js
// eslint-disable-next-line github/unescaped-html-literal
if (htmlString.startsWith('<tr')) {
if (startsWithTag(htmlString, 'tr')) {
const container = document.createElement('table');
container.innerHTML = htmlString;
return container.querySelector<T>('tr')!;
@@ -2,12 +2,13 @@ import emojis from '../../../assets/emoji.json' with {type: 'json'};
import {GET} from '../modules/fetch.ts';
import {showErrorToast} from '../modules/toast.ts';
import {parseIssuePageInfo} from '../utils.ts';
import {errorMessage} from '../modules/errors.ts';
import type {Issue, Mention} from '../types.ts';
const maxMatches = 6;
function sortAndReduce<T>(map: Map<T, number>): T[] {
const sortedMap = new Map(Array.from(map.entries()).sort((a, b) => a[1] - b[1]));
const sortedMap = new Map(Array.from(map).sort((a, b) => a[1] - b[1]));
return Array.from(sortedMap.keys()).slice(0, maxMatches);
}
@@ -47,7 +48,7 @@ export function fetchMentions(mentionsUrl: string): Promise<Mention[]> {
if (!res.ok) throw new Error(res.statusText);
return await res.json() as Mention[];
} catch (e) {
showErrorToast(`Failed to load mentions: ${e}`);
showErrorToast(`Failed to load mentions: ${errorMessage(e)}`);
return [];
}
})();
@@ -0,0 +1,13 @@
import {cutString} from './string.ts';
test('cutString', () => {
let [before, after, ok] = cutString('a = b = c', '=');
expect(before).toBe('a ');
expect(after).toBe(' b = c');
expect(ok).toBe(true);
[before, after, ok] = cutString(' a ', '=');
expect(before).toBe(' a ');
expect(after).toBe('');
expect(ok).toBe(false);
});
@@ -0,0 +1,5 @@
export function cutString(s: string, sep: string): [string, string, boolean] {
const index = s.indexOf(sep);
if (index === -1) return [s, '', false];
return [s.substring(0, index), s.substring(index + sep.length), true];
}
@@ -10,13 +10,21 @@ export function dedent(str: string) {
const match = str.match(/^[ \t]*(?=\S)/gm);
if (!match) return str;
let minIndent = Number.POSITIVE_INFINITY;
let minIndent = Infinity;
for (const indent of match) {
minIndent = Math.min(minIndent, indent.length);
}
if (minIndent === 0 || minIndent === Number.POSITIVE_INFINITY) {
if (minIndent === 0 || minIndent === Infinity) {
return str;
}
return str.replace(new RegExp(`^[ \\t]{${minIndent}}`, 'gm'), '').trim();
}
export function normalizeTestHtml(s: string) {
const lines = s.replace(/>\s+</g, '>\n<').trim().split('\n');
for (let i = 0; i < lines.length; i++) {
lines[i] = lines[i].trim();
}
return lines.join('\n');
}
+14 -7
View File
@@ -37,7 +37,7 @@ export function firstStartDateAfterDate(inputDate: Date): number {
}
const dayOfWeek = inputDate.getUTCDay();
const daysUntilSunday = 7 - dayOfWeek;
const resultDate = new Date(inputDate.getTime());
const resultDate = new Date(inputDate);
resultDate.setUTCDate(resultDate.getUTCDate() + daysUntilSunday);
return resultDate.valueOf();
}
@@ -65,18 +65,25 @@ export function fillEmptyStartDaysWithZeroes(startDays: number[], data: DayDataO
let dateFormat: Intl.DateTimeFormat;
/** Format a Date object to document's locale, but with 24h format from user's current locale because this
* option is a personal preference of the user, not something that the document's locale should dictate. */
// ISO 8601 UTC with 7-digit fractional seconds, matching Go's `2006-01-02T15:04:05.0000000Z07:00`
export function formatDatetimeISO(unixSeconds: number): string {
const base = new Date(unixSeconds * 1000).toISOString().slice(0, 19);
const frac = unixSeconds - Math.floor(unixSeconds);
const fracInt = Math.floor(frac * 10_000_000);
return `${base}.${String(fracInt).padStart(7, '0')}Z`;
}
/** Format a Date to a localized format, for example "21 May 2026, 14:30:45". */
export function formatDatetime(date: Date | number): string {
if (!dateFormat) {
// TODO: replace `hour12` with `Intl.Locale.prototype.getHourCycles` once there is broad browser support
dateFormat = new Intl.DateTimeFormat(getCurrentLocale(), {
day: 'numeric',
day: '2-digit',
month: 'short',
year: 'numeric',
hour: 'numeric',
hour12: !Number.isInteger(Number(new Intl.DateTimeFormat([], {hour: 'numeric'}).format())),
hour: '2-digit',
hourCycle: new Intl.DateTimeFormat([], {hour: 'numeric'}).resolvedOptions().hourCycle === 'h12' ? 'h12' : 'h23',
minute: '2-digit',
second: '2-digit',
});
}
return dateFormat.format(date);
@@ -1,4 +1,4 @@
import {linkifyURLs, pathEscape, pathEscapeSegments, urlQueryEscape} from './url.ts';
import {pathEscape, pathEscapeSegments, trimUrlPunctuation, urlQueryEscape, urlRawRegex} from './url.ts';
describe('escape', () => {
const queryNonAscii = " !\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~";
@@ -19,29 +19,36 @@ describe('escape', () => {
});
});
test('linkifyURLs', () => {
const link = (url: string) => `<a href="${url}" target="_blank">${url}</a>`;
expect(linkifyURLs('https://example.com')).toEqual(link('https://example.com'));
expect(linkifyURLs('https://dl.google.com/go/go1.23.6.linux-amd64.tar.gz')).toEqual(link('https://dl.google.com/go/go1.23.6.linux-amd64.tar.gz'));
expect(linkifyURLs('https://example.com/path?query=1&amp;b=2#frag')).toEqual(link('https://example.com/path?query=1&amp;b=2#frag'));
expect(linkifyURLs('visit https://example.com/repo for info')).toEqual(`visit ${link('https://example.com/repo')} for info`);
expect(linkifyURLs('See https://example.com.')).toEqual(`See ${link('https://example.com')}.`);
expect(linkifyURLs('https://example.com, and more')).toEqual(`${link('https://example.com')}, and more`);
expect(linkifyURLs('<span class="ansi-green-fg">https://proxy.golang.org/cached-only</span>')).toEqual(`<span class="ansi-green-fg">${link('https://proxy.golang.org/cached-only')}</span>`);
expect(linkifyURLs('<span style="color:rgb(0,255,0)">https://registry.npmjs.org/@types/node</span>')).toEqual(`<span style="color:rgb(0,255,0)">${link('https://registry.npmjs.org/@types/node')}</span>`);
expect(linkifyURLs('https://a.com and https://b.org')).toEqual(`${link('https://a.com')} and ${link('https://b.org')}`);
expect(linkifyURLs('no urls here')).toEqual('no urls here');
expect(linkifyURLs('http://example.com/path')).toEqual(link('http://example.com/path'));
expect(linkifyURLs('http://localhost:3000/repo')).toEqual(link('http://localhost:3000/repo'));
expect(linkifyURLs('https://')).toEqual('https://');
expect(linkifyURLs('<a href="https://example.com">Click here</a>')).toEqual('<a href="https://example.com">Click here</a>');
expect(linkifyURLs('<a\nhref="https://example.com">Click here</a>')).toEqual('<a\nhref="https://example.com">Click here</a>');
expect(linkifyURLs('<a href="https://example.com">https://example.com</a>')).toEqual('<a href="https://example.com">https://example.com</a>');
expect(linkifyURLs('https://evil.com/<script>alert(1)</script>')).toEqual(`${link('https://evil.com/')}<script>alert(1)</script>`);
expect(linkifyURLs('https://evil.com/"onmouseover="alert(1)')).toEqual(`${link('https://evil.com/')}"onmouseover="alert(1)`);
expect(linkifyURLs('javascript:alert(1)')).toEqual('javascript:alert(1)'); // eslint-disable-line no-script-url
expect(linkifyURLs("https://evil.com/'onclick='alert(1)")).toEqual(`${link('https://evil.com/')}'onclick='alert(1)`);
expect(linkifyURLs('data:text/html,<script>alert(1)</script>')).toEqual('data:text/html,<script>alert(1)</script>');
expect(linkifyURLs('https://evil.com/\nonclick=alert(1)')).toEqual(`${link('https://evil.com/')}\nonclick=alert(1)`);
expect(linkifyURLs('https://evil.com/&#34;onmouseover=alert(1)')).toEqual(`${link('https://evil.com/&#34;onmouseover=alert')}(1)`);
test('matchUrls', () => {
const matchUrls = (text: string) => Array.from(text.matchAll(urlRawRegex()), (m) => trimUrlPunctuation(m[0]));
expect(matchUrls('visit https://example.com for info')).toEqual(['https://example.com']);
expect(matchUrls('see https://example.com.')).toEqual(['https://example.com']);
expect(matchUrls('see https://example.com, and')).toEqual(['https://example.com']);
expect(matchUrls('see https://example.com; and')).toEqual(['https://example.com']);
expect(matchUrls('(https://example.com)')).toEqual(['https://example.com']);
expect(matchUrls('"https://example.com"')).toEqual(['https://example.com']);
expect(matchUrls('https://example.com/path?q=1&b=2#hash')).toEqual(['https://example.com/path?q=1&b=2#hash']);
expect(matchUrls('https://example.com/path?q=1&b=2#hash.')).toEqual(['https://example.com/path?q=1&b=2#hash']);
expect(matchUrls('https://x.co')).toEqual(['https://x.co']);
expect(matchUrls('https://example.com/path_(wiki)')).toEqual(['https://example.com/path_(wiki)']);
expect(matchUrls('https://en.wikipedia.org/wiki/Rust_(programming_language)')).toEqual(['https://en.wikipedia.org/wiki/Rust_(programming_language)']);
expect(matchUrls('(https://en.wikipedia.org/wiki/Rust_(programming_language))')).toEqual(['https://en.wikipedia.org/wiki/Rust_(programming_language)']);
expect(matchUrls('http://example.com')).toEqual(['http://example.com']);
expect(matchUrls('no url here')).toEqual([]);
expect(matchUrls('https://a.com and https://b.com')).toEqual(['https://a.com', 'https://b.com']);
expect(matchUrls('[![](https://img.shields.io/npm/v/pkg.svg?style=flat)](https://www.npmjs.org/package/pkg)')).toEqual(['https://img.shields.io/npm/v/pkg.svg?style=flat', 'https://www.npmjs.org/package/pkg']);
});
test('trimUrlPunctuation', () => {
expect(trimUrlPunctuation('https://example.com.')).toEqual('https://example.com');
expect(trimUrlPunctuation('https://example.com,')).toEqual('https://example.com');
expect(trimUrlPunctuation('https://example.com;')).toEqual('https://example.com');
expect(trimUrlPunctuation('https://example.com:')).toEqual('https://example.com');
expect(trimUrlPunctuation("https://example.com'")).toEqual('https://example.com');
expect(trimUrlPunctuation('https://example.com"')).toEqual('https://example.com');
expect(trimUrlPunctuation('https://example.com.,;')).toEqual('https://example.com');
expect(trimUrlPunctuation('https://example.com/path')).toEqual('https://example.com/path');
expect(trimUrlPunctuation('https://example.com/path_(wiki)')).toEqual('https://example.com/path_(wiki)');
expect(trimUrlPunctuation('https://example.com)')).toEqual('https://example.com');
expect(trimUrlPunctuation('https://en.wikipedia.org/wiki/Rust_(lang))')).toEqual('https://en.wikipedia.org/wiki/Rust_(lang)');
});
+13 -28
View File
@@ -1,3 +1,16 @@
/** Matches URLs, excluding characters that are never valid unencoded in URLs per RFC 3986. */
export const urlRawRegex = () => /\bhttps?:\/\/[^\s<>[\]]+/gi; // JS regexp has internal states, so always use a new instance
/** Strip trailing punctuation that is likely not part of the URL. */
export function trimUrlPunctuation(url: string): string {
url = url.replace(/[.,;:'"]+$/, '');
// Strip trailing closing parens only if unbalanced (not part of the URL like Wikipedia links)
while (url.endsWith(')') && (url.match(/\(/g) || []).length < (url.match(/\)/g) || []).length) {
url = url.slice(0, -1);
}
return url;
}
export function urlQueryEscape(s: string) {
// See "TestQueryEscape" in backend
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent#encoding_for_rfc3986
@@ -29,31 +42,3 @@ export function pathEscapeSegments(s: string): string {
// The same as backend's PathEscapeSegments
return s.split('/').map(pathEscape).join('/');
}
// Match HTML tags (to skip) or URLs (to linkify) in HTML content
const urlLinkifyPattern = /(<([-\w]+)[^>]*>)|(<\/([-\w]+)[^>]*>)|(https?:\/\/[^\s<>"'`|(){}[\]]+)/gi;
const trailingPunctPattern = /[.,;:!?]+$/;
// Convert URLs to clickable links in HTML, preserving existing HTML tags
export function linkifyURLs(html: string): string {
let inAnchor = false;
return html.replace(urlLinkifyPattern, (match, _openTagFull, openTag, _closeTagFull, closeTag, url) => {
// skip URLs inside existing <a> tags
if (openTag === 'a') {
inAnchor = true;
return match;
} else if (closeTag === 'a') {
inAnchor = false;
return match;
}
if (inAnchor || !url) {
return match;
}
const trailingPunct = url.match(trailingPunctPattern);
const cleanUrl = trailingPunct ? url.slice(0, -trailingPunct[0].length) : url;
const trailing = trailingPunct ? trailingPunct[0] : '';
// safe because regexp only matches valid URLs (no quotes or angle brackets)
return `<a href="${cleanUrl}" target="_blank">${cleanUrl}</a>${trailing}`; // eslint-disable-line github/unescaped-html-literal
});
}