fix(editor): database behavier (#14394)

fix #13459
fix #13707
fix #13924

#### PR Dependency Tree


* **PR #14394** 👈

This tree was auto-generated by
[Charcoal](https://github.com/danerwilliams/charcoal)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Improved URL paste: text is split into segments, inserted correctly,
and single-URL pastes create linked-page references.

* **UI Improvements**
  * Redesigned layout selector with compact dynamic options.
* Number-format options are always available in table headers and mobile
menus.

* **Bug Fixes**
  * More consistent paste behavior for mixed text+URL content.
  * Prevented recursive selection updates when exiting edit mode.

* **Tests**
* Added tests for URL splitting, paste insertion, number formatting, and
selection behavior.

* **Chores**
* Removed number-formatting feature flag; formatting now applied by
default.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
DarkSky
2026-02-08 23:31:30 +08:00
committed by GitHub
parent 8192a492d9
commit bb01bb1aef
20 changed files with 665 additions and 296 deletions
+95 -16
View File
@@ -95,28 +95,107 @@ export function isValidUrl(str: string, baseUrl = location.origin) {
return result?.allowed ?? false;
}
const URL_SCHEME_IN_TOKEN_REGEXP =
/(?:https?:\/\/|ftp:\/\/|sftp:\/\/|mailto:|tel:|www\.)/i;
const URL_LEADING_DELIMITER_REGEXP = /^[-([{<'"~]+/;
const URL_TRAILING_DELIMITER_REGEXP = /[)\]}>.,;:!?'"]+$/;
export type UrlTextSegment = {
text: string;
link?: string;
};
function appendUrlTextSegment(
segments: UrlTextSegment[],
segment: UrlTextSegment
) {
if (!segment.text) return;
const last = segments[segments.length - 1];
if (last && !last.link && !segment.link) {
last.text += segment.text;
return;
}
segments.push(segment);
}
function splitTokenByUrl(token: string, baseUrl: string): UrlTextSegment[] {
const schemeMatch = token.match(URL_SCHEME_IN_TOKEN_REGEXP);
const schemeIndex = schemeMatch?.index;
if (typeof schemeIndex === 'number' && schemeIndex > 0) {
return [
{ text: token.slice(0, schemeIndex) },
...splitTokenByUrl(token.slice(schemeIndex), baseUrl),
];
}
const leading = token.match(URL_LEADING_DELIMITER_REGEXP)?.[0] ?? '';
const withoutLeading = token.slice(leading.length);
const trailing =
withoutLeading.match(URL_TRAILING_DELIMITER_REGEXP)?.[0] ?? '';
const core = trailing
? withoutLeading.slice(0, withoutLeading.length - trailing.length)
: withoutLeading;
if (core && isValidUrl(core, baseUrl)) {
const segments: UrlTextSegment[] = [];
appendUrlTextSegment(segments, { text: leading });
appendUrlTextSegment(segments, { text: core, link: normalizeUrl(core) });
appendUrlTextSegment(segments, { text: trailing });
return segments;
}
return [{ text: token }];
}
/**
* Split plain text into mixed segments, where only URL segments carry link metadata.
* This is used by paste handlers so text like `example:https://google.com` keeps
* normal text while only URL parts are linkified.
*/
export function splitTextByUrl(text: string, baseUrl = location.origin) {
const chunks = text.match(/\s+|\S+/g);
if (!chunks) {
return [];
}
const segments: UrlTextSegment[] = [];
chunks.forEach(chunk => {
if (/^\s+$/.test(chunk)) {
appendUrlTextSegment(segments, { text: chunk });
return;
}
splitTokenByUrl(chunk, baseUrl).forEach(segment => {
appendUrlTextSegment(segments, segment);
});
});
return segments;
}
// https://en.wikipedia.org/wiki/Top-level_domain
const COMMON_TLDS = new Set([
'com',
'org',
'net',
'edu',
'gov',
'co',
'io',
'me',
'moe',
'mil',
'top',
'dev',
'xyz',
'info',
'cat',
'ru',
'co',
'com',
'de',
'dev',
'edu',
'eu',
'gov',
'info',
'io',
'jp',
'uk',
'me',
'mil',
'moe',
'net',
'org',
'pro',
'ru',
'top',
'uk',
'xyz',
]);
function isCommonTLD(url: URL) {