ci: add oxlint correctness check and resolve its findings (#592)
* refactor: resolve oxlint correctness findings without behavior changes - Snapshot iterations that mutate their collection with Array.from() instead of a spread (live childNodes, self-removing disposers, a Set extended inside its own loop). - Drop empty spread fallbacks, redundant regex escapes, Boolean() casts inside negations, a rethrow-only try/catch and statement ternaries. - Use startsWith/endsWith for anchored literal checks in the extension. - List U+FE0F explicitly in the mobile attachment emoji class; the matched set is unchanged. - Fix the WeChat copy fixture, where \oint in a template literal lost its backslash and the block formula never exercised \oint. - Document the intentional control-character regexes (filename sanitizing and input validation), the throwing provider stream test, Astro's generated type reference and the mobile auto-sync finally block with narrow inline exceptions. * ci: run oxlint correctness rules on pull requests Pin oxlint 1.88.0 and enable only its correctness category across apps/ and packages/, excluding build output, vendored code and the iOS app. Findings are errors and the script passes --deny-warnings, so CI fails on any finding. no-unused-vars stays off until unused code is removed in a dedicated pass.
This commit is contained in:
1 parent
c2ff5ca1b6
commit
bec688b91a
40 files changed
+139
-53
No files matched your search
@@ -1,6 +1,6 @@
|
||||
name: Test
|
||||
|
||||
# Runs the non-E2E regression suite, type checks and the web build for every
|
||||
# Runs the non-E2E regression suite, lint, type checks and the web build for every
|
||||
# pull request and every push to main, so failures surface before a release
|
||||
# instead of in the release workflow.
|
||||
on:
|
||||
@@ -52,6 +52,9 @@ jobs:
|
||||
sleep "$((15 * attempt))"
|
||||
done
|
||||
|
||||
- name: Lint
|
||||
run: bun run lint
|
||||
|
||||
- name: Type check
|
||||
run: bun run typecheck
|
||||
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"$schema": "./node_modules/oxlint/configuration_schema.json",
|
||||
"plugins": ["eslint", "typescript", "unicorn", "oxc"],
|
||||
"categories": {
|
||||
"correctness": "error"
|
||||
},
|
||||
"rules": {
|
||||
// Unused code is removed in a dedicated pass before this rule is enforced.
|
||||
"no-unused-vars": "off"
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
// Print windows deliberately load a preload that exposes no desktop bridge.
|
||||
"files": ["apps/desktop/src/preload/print.cjs"],
|
||||
"rules": { "unicorn/no-empty-file": "off" }
|
||||
}
|
||||
],
|
||||
"ignorePatterns": [
|
||||
"**/dist/**",
|
||||
"**/vendor/**",
|
||||
"apps/ios/**",
|
||||
"apps/site/.astro/**",
|
||||
"apps/extension/dist-firefox/**",
|
||||
"packages/plugin-api/dist/**"
|
||||
]
|
||||
}
|
||||
@@ -398,6 +398,7 @@ describe("companion HTTP contracts", () => {
|
||||
expect((await getCompanionTurn(db, scope, payload.id))).toMatchObject({ status: "failed", response: "partial response" });
|
||||
});
|
||||
test("maps provider HTTP 400 without leaking the response body", async () => {
|
||||
// oxlint-disable-next-line require-yield -- the provider stream fails before its first chunk
|
||||
const { request } = fixture({ stream: async () => ({ totalUsage: Promise.resolve({}), fullStream: (async function* () {
|
||||
const error = new Error("Invalid schema sk-secret");
|
||||
error.statusCode = 400;
|
||||
|
||||
@@ -45,12 +45,12 @@ const mcpRequest = (payload, headers = {}) => ({
|
||||
|
||||
const modernMcpRequest = (payload, headers = {}) => {
|
||||
const params = {
|
||||
...(payload.params ?? {}),
|
||||
...payload.params,
|
||||
_meta: {
|
||||
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
|
||||
"io.modelcontextprotocol/clientInfo": { name: "edgeever-test", version: "1.0.0" },
|
||||
"io.modelcontextprotocol/clientCapabilities": {},
|
||||
...(payload.params?._meta ?? {}),
|
||||
...payload.params?._meta,
|
||||
},
|
||||
};
|
||||
const name = payload.method === "resources/read" ? params.uri : params.name;
|
||||
|
||||
@@ -129,6 +129,7 @@ export const validateAttachmentUpload = (size: number) => {
|
||||
export const normalizeFilename = (filename: string) => filename
|
||||
.trim()
|
||||
.replace(/[\\/]/g, "-")
|
||||
// oxlint-disable-next-line no-control-regex -- strips control characters from resource filenames
|
||||
.replace(/[\u0000-\u001f\u007f]/g, "")
|
||||
.slice(0, 160);
|
||||
|
||||
|
||||
@@ -115,7 +115,8 @@ const runNpm = (args, cwd, { executablePath = process.execPath, spawnImpl = spaw
|
||||
child.once("error", (error) => { clearTimeout(timer); reject(error); });
|
||||
child.once("exit", (code) => {
|
||||
clearTimeout(timer);
|
||||
code === 0 ? resolve() : reject(new Error("adapter_install_failed"));
|
||||
if (code === 0) resolve();
|
||||
else reject(new Error("adapter_install_failed"));
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -34,6 +34,7 @@ export const resourceIdFromRequest = (requestUrl) => {
|
||||
const normalizeDownloadFilename = (filename) => filename
|
||||
.trim()
|
||||
.replace(/[\\/]/g, "-")
|
||||
// oxlint-disable-next-line no-control-regex -- strips control characters from resource filenames
|
||||
.replace(/[\u0000-\u001f\u007f]/g, "")
|
||||
.slice(0, 160);
|
||||
|
||||
|
||||
@@ -13,7 +13,9 @@ export const normalizeStagedResourceMetadataInput = (input) => {
|
||||
const type = typeof input.type === "string" && input.type.trim() ? input.type.trim() : "application/octet-stream";
|
||||
const size = Number(input.size);
|
||||
if (!memoId || memoId.length > MAX_STAGED_RESOURCE_MEMO_ID_LENGTH) throw new Error("Invalid staged resource memo id");
|
||||
// oxlint-disable-next-line no-control-regex -- rejects control characters in staged resource names
|
||||
if (!name || name.length > MAX_STAGED_RESOURCE_NAME_LENGTH || /[\u0000-\u001f\u007f]/.test(name)) throw new Error("Invalid staged resource name");
|
||||
// oxlint-disable-next-line no-control-regex -- rejects control characters in staged resource types
|
||||
if (type.length > MAX_STAGED_RESOURCE_TYPE_LENGTH || /[\u0000-\u001f\u007f]/.test(type)) throw new Error("Invalid staged resource type");
|
||||
if (!Number.isSafeInteger(size) || size <= 0 || size > MAX_STAGED_RESOURCE_BYTES) {
|
||||
throw new Error("Staged resource must be between 1 byte and 1 GiB");
|
||||
|
||||
@@ -235,7 +235,7 @@ test("Douban short comments bind to the chosen own permalink and do not capture
|
||||
|
||||
|
||||
test("Jike declared content cannot lose its entire text or links, and media cannot be replaced by duplicates",()=>{
|
||||
for(const mode of ["remove","hidden"]){const doc=fixture("jike-grid"),text=doc.querySelector('.text');mode==="remove"?text.remove():text.setAttribute('hidden','');expect(read("jike-grid",doc)).toEqual({ok:false,reason:"incomplete"});}
|
||||
for(const mode of ["remove","hidden"]){const doc=fixture("jike-grid"),text=doc.querySelector('.text');if(mode==="remove")text.remove();else text.setAttribute('hidden','');expect(read("jike-grid",doc)).toEqual({ok:false,reason:"incomplete"});}
|
||||
const links=fixture("jike-long");for(const link of Array.from(links.querySelectorAll('.text a[href]')))link.remove();expect(read("jike-long",links)).toEqual({ok:false,reason:"incomplete"});
|
||||
const wrong=fixture("jike-long");wrong.querySelector('.text a[href]').setAttribute('href','https://m.okjike.com/originalPosts/6715e824d9dc81724c2ec74d');expect(read("jike-long",wrong)).toEqual({ok:false,reason:"incomplete"});
|
||||
const images=fixture("jike-grid"),first=images.querySelector('.image-item img').getAttribute('src');for(const img of Array.from(images.querySelectorAll('.image-item img')))img.setAttribute('src',first);expect(read("jike-grid",images)).toEqual({ok:false,reason:"incomplete"});
|
||||
@@ -244,7 +244,7 @@ test("Jike declared content cannot lose its entire text or links, and media cann
|
||||
test("hidden visual date and rating fields are omitted while optional short-comment time does not gate text",()=>{
|
||||
const doc=fixture("douban-book");doc.querySelector('.main-meta').textContent='';const time=doc.createElement('span');time.setAttribute('content','2024-07-31');time.setAttribute('hidden','');time.textContent='2024-07-31 17:37:35';doc.querySelector('.main-meta').appendChild(time);
|
||||
const rating=doc.querySelector('.main-title-rating');rating.setAttribute('title','HIDDEN RATING');rating.setAttribute('hidden','');const result=read('douban-book',doc);expect(result.clip.publishedAt).toBeUndefined();expect(result.clip.markdown).not.toContain('HIDDEN RATING');
|
||||
for(const action of ['remove','href']){const list=fixture('douban-comments'),card=list.querySelector('.comment-item'),time=card.querySelector('.comment-time');action==='remove'?time.remove():time.removeAttribute('href');const target=doubanCommentTarget(card,'https://book.douban.com/subject/36864764/comments/');const saved=readDouban(list,target,'https://book.douban.com/subject/36864764/comments/');expect(saved.ok).toBe(true);expect(saved.clip.markdown).toContain('/subject/36864764/comments/#c-4237804504');}
|
||||
for(const action of ['remove','href']){const list=fixture('douban-comments'),card=list.querySelector('.comment-item'),time=card.querySelector('.comment-time');if(action==='remove')time.remove();else time.removeAttribute('href');const target=doubanCommentTarget(card,'https://book.douban.com/subject/36864764/comments/');const saved=readDouban(list,target,'https://book.douban.com/subject/36864764/comments/');expect(saved.ok).toBe(true);expect(saved.clip.markdown).toContain('/subject/36864764/comments/#c-4237804504');}
|
||||
});
|
||||
|
||||
const jikeFeedUrl='https://m.okjike.com/topics/63579abb6724cc583b9bba9a';
|
||||
|
||||
@@ -185,7 +185,7 @@ const cleanLicense = (value: string) => collapse(value).replace(/\s+license$/i,
|
||||
|
||||
const cleanLanguage = (value: string) => {
|
||||
const text = collapse(value).replace(/\s+\d+(?:\.\d+)?%$/, "").trim();
|
||||
if (!text || text.length > 40 || /%$/.test(text) || /^languages?$/i.test(text)) return "";
|
||||
if (!text || text.length > 40 || text.endsWith("%") || /^languages?$/i.test(text)) return "";
|
||||
return text;
|
||||
};
|
||||
|
||||
|
||||
@@ -65,7 +65,7 @@ export const platformTarget = (pageUrl: string): PlatformTarget | null => {
|
||||
return id ? { platform: "xiaoyuzhou", id, url: `https://www.xiaoyuzhoufm.com/episode/${id}` } : null;
|
||||
}
|
||||
if (url.hostname === "www.zhipin.com" || url.hostname === "zhipin.com") {
|
||||
const id = /^\/job_detail\/([a-zA-Z0-9_~\-]+)\.html$/.exec(url.pathname)?.[1];
|
||||
const id = /^\/job_detail\/([a-zA-Z0-9_~-]+)\.html$/.exec(url.pathname)?.[1];
|
||||
return id ? { platform: "boss", id, url: `https://www.zhipin.com/job_detail/${id}.html` } : null;
|
||||
}
|
||||
if (url.hostname === "xueqiu.com" || url.hostname === "www.xueqiu.com") {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/** Plain-text fallback when the clicked selection no longer has matching DOM. */
|
||||
export const selectionTextMarkdown = (value: string) => value
|
||||
// Entities keep Markdown, HTML and automatic links from reinterpreting literal text.
|
||||
.replace(/[!-/:-@\[-`{-~]/g, (character) => `&#${character.charCodeAt(0)};`)
|
||||
.replace(/[!-/:-@[-`{-~]/g, (character) => `&#${character.charCodeAt(0)};`)
|
||||
.replace(/^[ \t]+/gm, (indent) => indent.replace(/ /g, " ").replace(/\t/g, "	"));
|
||||
|
||||
const escapeMarkdownLabel = (value: string) =>
|
||||
|
||||
@@ -455,7 +455,7 @@ export const readLinkedin = (doc: Document, target: PlatformTarget): PlatformRea
|
||||
link.closest(LINKEDIN_CARD_SELECTOR) === card && platformText(link));
|
||||
const person = actorLinks.find(link => {
|
||||
const url = safePlatformUrl(link.getAttribute('href') || '', target.url);
|
||||
return url && /(^|\.)linkedin\.com$/.test(new URL(url).hostname) && /^\/in\//.test(new URL(url).pathname);
|
||||
return url && /(^|\.)linkedin\.com$/.test(new URL(url).hostname) && new URL(url).pathname.startsWith("/in/");
|
||||
});
|
||||
const author = text(record(schema?.author)?.name) || platformText(own('.update-components-actor__title, [data-tracking-control-name="public_post_feed-actor-name"]'))
|
||||
|| platformText(person?.querySelector('p') || person) || actorLinks.map(platformText).find(Boolean);
|
||||
|
||||
@@ -71,7 +71,7 @@ export const readXueqiu = (doc: Document, target: PlatformTarget, sourceUrl: str
|
||||
const title = (titleNode ? platformText(platformReadableCopy(titleNode)) : "") || platformText(clean).slice(0, 120);
|
||||
if (!markdown || !title) return { ok: false, reason: "unreadable" };
|
||||
const cleanTime = timeLink ? platformReadableCopy(timeLink) : null;
|
||||
const published = cleanTime && /^发布于/.test(platformText(cleanTime)) ? publishedTime(cleanTime.querySelector("time")) : undefined;
|
||||
const published = cleanTime && platformText(cleanTime).startsWith("发布于") ? publishedTime(cleanTime.querySelector("time")) : undefined;
|
||||
return { ok: true, clip: { ...target, title, author: author ? platformText(platformReadableCopy(author)) || undefined : undefined, publishedAt: published,
|
||||
markdown, tags: ["web-clip", "xueqiu", "投资", ...xueqiuStocks(clean, target.url)], completeness: "complete" } };
|
||||
};
|
||||
@@ -1718,9 +1718,9 @@ function LocalTiptapEditorImpl(props: LocalTiptapEditorProps) {
|
||||
disabled={(action.id === "undo" && ((historyState ?? 0) & 1) === 0)
|
||||
|| (action.id === "redo" && ((historyState ?? 0) & 2) === 0)
|
||||
|| (action.id === "increaseListIndent"
|
||||
&& !Boolean(editor?.can().chain().focus().sinkListItem(activeListItemType).run()))
|
||||
&& !editor?.can().chain().focus().sinkListItem(activeListItemType).run())
|
||||
|| (action.id === "decreaseListIndent"
|
||||
&& !Boolean(editor?.can().chain().focus().liftListItem(activeListItemType).run()))}
|
||||
&& !editor?.can().chain().focus().liftListItem(activeListItemType).run())}
|
||||
icon={toolbarIcons[action.id]}
|
||||
label={getMobileEditorToolbarActionLabel(action.id, props.locale)}
|
||||
onRun={toolbarHandlers[action.id]}
|
||||
|
||||
@@ -112,6 +112,7 @@ export const useMobileAutomaticSync = ({
|
||||
requestedRef.current = false;
|
||||
requestedForceRef.current = false;
|
||||
void runRef.current(nextForce);
|
||||
// oxlint-disable-next-line no-unsafe-finally -- the catch above handles every error, so nothing is overridden; the finally block is restructured separately
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -67,7 +67,7 @@ const normalizeAttachmentFilename = (label: string, resourceId: string) => {
|
||||
const withoutPrefix = label.replace(/^\s*(?:附件[::]|Attachment:)\s*/i, "").trim();
|
||||
const colonParts = withoutPrefix.split(/[::]\s*/).filter(Boolean);
|
||||
const candidate = (colonParts.at(-1) || withoutPrefix)
|
||||
.replace(/^[\s📎📄📦📊🗃️🗂️]+/u, "")
|
||||
.replace(/^[\uFE0F\s📎📄📦📊🗃🗂]+/u, "")
|
||||
.trim();
|
||||
return candidate || resourceId;
|
||||
};
|
||||
@@ -225,6 +225,7 @@ export const getMobileAttachmentUpdatePayload = (contentJson: TiptapDoc) => ({
|
||||
export const getMobileResourceUpdatePayload = getMobileAttachmentUpdatePayload;
|
||||
|
||||
const safeCacheFilename = (filename: string) =>
|
||||
// oxlint-disable-next-line no-control-regex -- strips control characters from cached filenames
|
||||
filename.trim().replace(/[\\/]/g, "-").replace(/[\u0000-\u001f\u007f]/g, "").slice(0, 160) || "attachment";
|
||||
|
||||
const EXT_BY_MIME: Record<string, string> = {
|
||||
|
||||
@@ -12,7 +12,7 @@ export const Meteors = ({ number = 20, className }: MeteorsProps) => {
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const styles = [...new Array(number)].map(() => ({
|
||||
const styles = Array.from({ length: number }, () => ({
|
||||
top: 0,
|
||||
left: Math.floor(Math.random() * (400 - -400) + -400) + "px",
|
||||
animationDelay: Math.random() * (0.8 - 0.2) + 0.2 + "s",
|
||||
|
||||
Vendored
+1
@@ -1,2 +1,3 @@
|
||||
// oxlint-disable-next-line typescript/triple-slash-reference -- Astro loads its generated types this way
|
||||
/// <reference path="../.astro/types.d.ts" />
|
||||
/// <reference types="astro/client" />
|
||||
@@ -875,7 +875,7 @@ const bindDiagramScrollerFit = (graph: Graph) => {
|
||||
gridWidth as number | undefined,
|
||||
gridHeight,
|
||||
padding as number | undefined,
|
||||
applyDiagramScrollerFitOptions({ ...(options ?? {}) }, bounds),
|
||||
applyDiagramScrollerFitOptions({ ...options }, bounds),
|
||||
);
|
||||
}) as typeof graph.fitToContent & { edgeeverBound?: boolean };
|
||||
bound.edgeeverBound = true;
|
||||
@@ -1240,7 +1240,7 @@ const nodeMetadata = (
|
||||
text: diagramNodePresentation(node, kind).text,
|
||||
lineHeight: mindStyle?.visual.label.lineHeight ?? flowchartStyle?.label.lineHeight ?? architectureStyle?.label.lineHeight ?? 18,
|
||||
},
|
||||
...(architectureStyle?.attrs ?? {}),
|
||||
...architectureStyle?.attrs,
|
||||
},
|
||||
...(hasPorts ? { ports: {
|
||||
groups: {
|
||||
|
||||
@@ -1138,7 +1138,7 @@ export default function PosterEditorPane({
|
||||
const sourceSession = requiresLocalEditSession(source)
|
||||
? createLocalEditSession(source)
|
||||
: (await api.createMemoEditSession(source.id)).editSession;
|
||||
const markdown = `${source.contentMarkdown}\n\n![${title.replace(/[\[\]\\\n]/g, " ")}](${resource.url})`;
|
||||
const markdown = `${source.contentMarkdown}\n\n![${title.replace(/[[\]\\\n]/g, " ")}](${resource.url})`;
|
||||
const result = await repository.updateMemo(source, {
|
||||
expectedRevision: source.revision,
|
||||
expectedContentHash: source.contentHash,
|
||||
|
||||
@@ -173,6 +173,7 @@ export const parseCustomEditorThemeFile = (source: string): PortableCustomEditor
|
||||
export const customEditorThemeFileName = (name: string) => {
|
||||
const safeName = name
|
||||
.trim()
|
||||
// oxlint-disable-next-line no-control-regex -- replaces control characters in theme filenames
|
||||
.replace(/[\u0000-\u001f<>:"/\\|?*]+/g, "-")
|
||||
.replace(/\s+/g, " ")
|
||||
.replace(/[. ]+$/g, "")
|
||||
|
||||
@@ -161,13 +161,9 @@ export const createDesktopRepository = (): EdgeEverRepository => ({
|
||||
return local;
|
||||
}
|
||||
if (typeof navigator !== "undefined" && !navigator.onLine) return local;
|
||||
try {
|
||||
const remote = await api.listTemplates();
|
||||
await Promise.all(remote.templates.map((template) => request("template.cache", { template })));
|
||||
return remote;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
const remote = await api.listTemplates();
|
||||
await Promise.all(remote.templates.map((template) => request("template.cache", { template })));
|
||||
return remote;
|
||||
},
|
||||
createTemplate: (input) => request("template.create", input).then((result) => {
|
||||
window.dispatchEvent(new CustomEvent("edgeever:sync-queue-changed"));
|
||||
|
||||
@@ -34,6 +34,7 @@ export class MarkdownExportMemoryLimitError extends Error {
|
||||
|
||||
export const sanitizeExportPathSegment = (value: string, fallback: string) => {
|
||||
const sanitized = value
|
||||
// oxlint-disable-next-line no-control-regex -- replaces control characters in export paths
|
||||
.replace(/[\u0000-\u001f<>:"/\\|?*]/g, "-")
|
||||
.replace(/\s+/g, " ")
|
||||
.replace(/[. ]+$/g, "")
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
const escapeLabel = (value: string) => value
|
||||
.replaceAll("\\", "\\\\")
|
||||
.replace(/[\[\]\r\n]/g, (character) => character === "\r" || character === "\n" ? " " : `\\${character}`);
|
||||
.replace(/[[\]\r\n]/g, (character) => character === "\r" || character === "\n" ? " " : `\\${character}`);
|
||||
|
||||
export const resourceToMarkdown = (resource: { kind: "image" | "attachment"; url: string; filename: string | null }, fallbackName: string) => {
|
||||
const label = escapeLabel(resource.filename || fallbackName || "file");
|
||||
|
||||
@@ -66,6 +66,7 @@ export const getHtmlImageEmbedNoticeKind = (
|
||||
|
||||
export const buildHtmlFilename = (title: string, fallback: string) => {
|
||||
const sanitized = title
|
||||
// oxlint-disable-next-line no-control-regex -- replaces control characters in export filenames
|
||||
.replace(/[\u0000-\u001f<>:"/\\|?*]/g, "-")
|
||||
.replace(/\s+/g, " ")
|
||||
.replace(/[. ]+$/g, "")
|
||||
|
||||
@@ -14,6 +14,7 @@ const WINDOWS_RESERVED_NAME = /^(con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/i;
|
||||
|
||||
export const buildMarkdownFilename = (title: string, fallback: string) => {
|
||||
const sanitized = title
|
||||
// oxlint-disable-next-line no-control-regex -- replaces control characters in export filenames
|
||||
.replace(/[\u0000-\u001f<>:"/\\|?*]/g, "-")
|
||||
.replace(/\s+/g, " ")
|
||||
.replace(/[. ]+$/g, "")
|
||||
|
||||
@@ -189,7 +189,7 @@ describe("EdgeEverPluginHost", () => {
|
||||
set: async (pluginId, key, value) => secrets.set(`${pluginId}:${key}`, value),
|
||||
remove: async (pluginId, key) => secrets.delete(`${pluginId}:${key}`),
|
||||
clearNamespace: async (pluginId) => {
|
||||
for (const key of [...secrets.keys()]) if (key.startsWith(`${pluginId}:`)) secrets.delete(key);
|
||||
for (const key of Array.from(secrets.keys())) if (key.startsWith(`${pluginId}:`)) secrets.delete(key);
|
||||
},
|
||||
};
|
||||
const packageStorage = {
|
||||
@@ -289,7 +289,7 @@ describe("EdgeEverPluginHost", () => {
|
||||
put: async (value) => packages.set(`${value.pluginId}:${value.version}`, value),
|
||||
remove: async (pluginId, version) => {
|
||||
if (version) packages.delete(`${pluginId}:${version}`);
|
||||
else for (const key of [...packages.keys()]) if (key.startsWith(`${pluginId}:`)) packages.delete(key);
|
||||
else for (const key of Array.from(packages.keys())) if (key.startsWith(`${pluginId}:`)) packages.delete(key);
|
||||
},
|
||||
};
|
||||
const secretStorage = {
|
||||
@@ -331,7 +331,7 @@ describe("EdgeEverPluginHost", () => {
|
||||
put: async (value) => packages.set(`${value.pluginId}:${value.version}`, value),
|
||||
remove: async (id, version) => {
|
||||
if (version) packages.delete(`${id}:${version}`);
|
||||
else for (const key of [...packages.keys()]) if (key.startsWith(`${id}:`)) packages.delete(key);
|
||||
else for (const key of Array.from(packages.keys())) if (key.startsWith(`${id}:`)) packages.delete(key);
|
||||
},
|
||||
};
|
||||
const host = new EdgeEverPluginHost({ repository, scope: "test", packageStorage });
|
||||
|
||||
@@ -1047,24 +1047,24 @@ export class EdgeEverPluginHost {
|
||||
try { dispose(); } catch { /* A broken disposer must not strand the plugin. */ }
|
||||
}
|
||||
try { await active.plugin.deactivate?.(); } catch { /* Cleanup must continue after a broken deactivate hook. */ }
|
||||
for (const key of [...this.commands.keys()]) {
|
||||
for (const key of Array.from(this.commands.keys())) {
|
||||
if (key.startsWith(`${pluginId}:`)) this.commands.delete(key);
|
||||
}
|
||||
for (const key of [...this.panels.keys()]) {
|
||||
for (const key of Array.from(this.panels.keys())) {
|
||||
if (key.startsWith(`${pluginId}:`)) this.panels.delete(key);
|
||||
}
|
||||
for (const key of [...this.embeds.keys()]) {
|
||||
for (const key of Array.from(this.embeds.keys())) {
|
||||
if (key.startsWith(`${pluginId}:`)) this.embeds.delete(key);
|
||||
}
|
||||
for (const [key, disposers] of [...this.mountedPanels]) {
|
||||
for (const [key, disposers] of Array.from(this.mountedPanels)) {
|
||||
if (!key.startsWith(`${pluginId}:`)) continue;
|
||||
for (const dispose of [...disposers]) {
|
||||
for (const dispose of Array.from(disposers)) {
|
||||
try { dispose(); } catch { /* Panel cleanup must not strand the plugin. */ }
|
||||
}
|
||||
}
|
||||
for (const [key, embedDisposers] of [...this.mountedEmbeds]) {
|
||||
for (const [key, embedDisposers] of Array.from(this.mountedEmbeds)) {
|
||||
if (!key.startsWith(`${pluginId}:`)) continue;
|
||||
for (const dispose of [...embedDisposers]) {
|
||||
for (const dispose of Array.from(embedDisposers)) {
|
||||
try { dispose(); } catch { /* Embed cleanup must not strand the plugin. */ }
|
||||
}
|
||||
}
|
||||
@@ -1618,7 +1618,7 @@ export class EdgeEverPluginHost {
|
||||
this.refreshSnapshot();
|
||||
const dispose = () => {
|
||||
this.embeds.delete(key);
|
||||
for (const mountedDispose of [...(this.mountedEmbeds.get(key) ?? [])]) mountedDispose();
|
||||
for (const mountedDispose of Array.from(this.mountedEmbeds.get(key) ?? [])) mountedDispose();
|
||||
this.refreshSnapshot();
|
||||
};
|
||||
disposers.push(dispose);
|
||||
|
||||
@@ -58,7 +58,7 @@ const MARKDOWN = `
|
||||
- **行内公式**:例如质能方程 $E = mc^2$。
|
||||
- **多行独立公式块**:
|
||||
$$
|
||||
\oint_{\\partial \\Omega} \\mathbf{E} \\cdot d\\mathbf{S} = \\frac{1}{\\varepsilon_0} \\iiint_{\\Omega} \\rho \\, dV
|
||||
\\oint_{\\partial \\Omega} \\mathbf{E} \\cdot d\\mathbf{S} = \\frac{1}{\\varepsilon_0} \\iiint_{\\Omega} \\rho \\, dV
|
||||
$$
|
||||
|
||||
- [x] 已完成的待办
|
||||
|
||||
@@ -84,6 +84,7 @@
|
||||
"fake-indexeddb": "^6.2.5",
|
||||
"google-auth-library": "^11.0.2",
|
||||
"linkedom": "0.18.13",
|
||||
"oxlint": "1.88.0",
|
||||
"tailwindcss": "^4.3.2",
|
||||
"tw-animate-css": "^1.4.0",
|
||||
"typescript": "6.0.3",
|
||||
@@ -101,7 +102,7 @@
|
||||
},
|
||||
"apps/desktop": {
|
||||
"name": "@edgeever/desktop",
|
||||
"version": "1.112.0",
|
||||
"version": "1.120.0",
|
||||
"dependencies": {
|
||||
"@agentclientprotocol/sdk": "^1.5.1",
|
||||
"@edgeever/public-network": "workspace:*",
|
||||
@@ -120,17 +121,17 @@
|
||||
"name": "@edgeever/extension",
|
||||
"version": "0.1.17",
|
||||
"dependencies": {
|
||||
"@joplin/turndown-plugin-gfm": "1.0.68",
|
||||
"@mixmark-io/domino": "2.2.0",
|
||||
"@mozilla/readability": "^0.6.0",
|
||||
"marked": "^18.0.5",
|
||||
"mdast-util-from-markdown": "2.0.3",
|
||||
"mdast-util-gfm": "3.1.0",
|
||||
"micromark-extension-gfm": "3.0.0",
|
||||
"react": "~19.2.7",
|
||||
"react-dom": "~19.2.7",
|
||||
"sonner": "2.0.8",
|
||||
"turndown": "^7.2.4",
|
||||
"@joplin/turndown-plugin-gfm": "1.0.68",
|
||||
"mdast-util-from-markdown": "2.0.3",
|
||||
"mdast-util-gfm": "3.1.0",
|
||||
"micromark-extension-gfm": "3.0.0",
|
||||
},
|
||||
"devDependencies": {
|
||||
"web-ext": "^10.7.0",
|
||||
@@ -331,9 +332,9 @@
|
||||
},
|
||||
"patchedDependencies": {
|
||||
"expo-sharing@57.0.22": "patches/expo-sharing@57.0.22.patch",
|
||||
"@joplin/turndown-plugin-gfm@1.0.68": "patches/@joplin%2Fturndown-plugin-gfm@1.0.68.patch",
|
||||
"mermaid@11.16.1": "patches/mermaid@11.16.1.patch",
|
||||
"npm@11.21.0": "patches/npm@11.21.0.patch",
|
||||
"@joplin/turndown-plugin-gfm@1.0.68": "patches/@joplin%2Fturndown-plugin-gfm@1.0.68.patch",
|
||||
},
|
||||
"overrides": {
|
||||
"dompurify": "3.4.16",
|
||||
@@ -1174,6 +1175,44 @@
|
||||
|
||||
"@oxc-project/types": ["@oxc-project/types@0.153.0", "", {}, "sha512-jSNrRIyvPVLL5E1z/tS5zLAprosGz/FuI1PWwEOz59j3E4TTTzSpqKb/RDGqJhhW0QHYISGXxq582wGx6T11Tw=="],
|
||||
|
||||
"@oxlint/binding-android-arm-eabi": ["@oxlint/binding-android-arm-eabi@1.88.0", "", { "os": "android", "cpu": "arm" }, "sha512-k6Fgkf86ubfeH4GvqYsRolfuVAp0pfYVZvifSlXNHPdMaWUOfxUrmLEeDD7hF6U2oJuhsVxhSWq86/xCJ+9MmA=="],
|
||||
|
||||
"@oxlint/binding-android-arm64": ["@oxlint/binding-android-arm64@1.88.0", "", { "os": "android", "cpu": "arm64" }, "sha512-AsIyhl9o1ejwtac7NMbhKpMdSml6NWR0ljKjZERxC8OHYDAW8o/Om9n1QHVLi4VCEUo42Zte2/Op9kyoGpJZMg=="],
|
||||
|
||||
"@oxlint/binding-darwin-arm64": ["@oxlint/binding-darwin-arm64@1.88.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-qgBjB7eEKcCB4RqjXGuoSZq74g0wD09fWFJ+HWcCJcKaga8TeUHtb+BY0SsxdleSLBgx4RZ7qmgo0EzqEq7JoQ=="],
|
||||
|
||||
"@oxlint/binding-darwin-x64": ["@oxlint/binding-darwin-x64@1.88.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-sCz3X5Mw/xdzEzn4fsCt1Iq8NKJnGNfhR+b9B5k+4DysYAHsmIhYoocnBPiQ9kvY3+pUun2A0Z8Vtun/Nv6YoQ=="],
|
||||
|
||||
"@oxlint/binding-freebsd-x64": ["@oxlint/binding-freebsd-x64@1.88.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-xTOdYOIqCufgIj0knvxu86FBfovP6QBiD7ngNva01gb8xPQCD5gXkE5APGczH35zStim1O3zqvFbLQmY85xXRg=="],
|
||||
|
||||
"@oxlint/binding-linux-arm-gnueabihf": ["@oxlint/binding-linux-arm-gnueabihf@1.88.0", "", { "os": "linux", "cpu": "arm" }, "sha512-BLadtw88NdYJBrAtC1dYzrPfSP2d5zgx6iuZycdD0YmZBJ2Nqbit9Z4PSaIb6gxK25dweArGMPbOmq/0ugwUPg=="],
|
||||
|
||||
"@oxlint/binding-linux-arm-musleabihf": ["@oxlint/binding-linux-arm-musleabihf@1.88.0", "", { "os": "linux", "cpu": "arm" }, "sha512-wILSczl/DaZsCcf7j7tvNbssp8hxEHIJxrv4j7+JjI3TAL1U/JTWbeUtoJJC/vo8+2hkTmHLYoKJjmiNbijdXw=="],
|
||||
|
||||
"@oxlint/binding-linux-arm64-gnu": ["@oxlint/binding-linux-arm64-gnu@1.88.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-jutT2Hy8JCuZuMwIFHo2HtyxfOfBJHyQxlBHBK1SL9W2f8n/WhjAP0FnDMz9d/mjhv9+QA0mx5AdQ9fDjsrFFg=="],
|
||||
|
||||
"@oxlint/binding-linux-arm64-musl": ["@oxlint/binding-linux-arm64-musl@1.88.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-8N8iGV9hh4Lkeuq7QsEkjoXNRRflqR1G2qyoGYXcA0k5qllrdqmfylKEXyHIVERxqNDyMO4StqDQ0uLxXcBQAg=="],
|
||||
|
||||
"@oxlint/binding-linux-ppc64-gnu": ["@oxlint/binding-linux-ppc64-gnu@1.88.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-5ydcQt3znNQ/rGEWYRvpS2cK9+T18P2VYgnyJ+eloEAwi9YUmsXy5VGrRqNWIK6qzWwHGnEE3ojVqqQNo+f0hA=="],
|
||||
|
||||
"@oxlint/binding-linux-riscv64-gnu": ["@oxlint/binding-linux-riscv64-gnu@1.88.0", "", { "os": "linux", "cpu": "none" }, "sha512-iGx0uvFT54b/5DPJdEBCxYIrVB3v0uTy+JhRh3CyEp8/22BXDkG8rffkFwREdLsdj9mOrPbJYVOZw8ipcBuzjw=="],
|
||||
|
||||
"@oxlint/binding-linux-riscv64-musl": ["@oxlint/binding-linux-riscv64-musl@1.88.0", "", { "os": "linux", "cpu": "none" }, "sha512-Wt47mKMWelVjRjuR7BCFxXkB0m79K1sTEbqMHRWciKUgo5iv7sWsF4RXSOyFYHD9zerNrkRZblnN273rhwOA1Q=="],
|
||||
|
||||
"@oxlint/binding-linux-s390x-gnu": ["@oxlint/binding-linux-s390x-gnu@1.88.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-g3L12KCS4+1gtUt3RbJ4F+Yxx4LNmmZLEPfhd8rAHEJjJk2lAgNfnjGoP/phJyhG5WS6VbNYbJtadJelNfvTMA=="],
|
||||
|
||||
"@oxlint/binding-linux-x64-gnu": ["@oxlint/binding-linux-x64-gnu@1.88.0", "", { "os": "linux", "cpu": "x64" }, "sha512-og/XHVDOD9ZJgRiZkT9DgzQd4sr+JZW7e4l17Q/jVMZiIbeEd4uIu9iAoUbTF1gIvVZoRc4QDOxyVUvQGp4WHg=="],
|
||||
|
||||
"@oxlint/binding-linux-x64-musl": ["@oxlint/binding-linux-x64-musl@1.88.0", "", { "os": "linux", "cpu": "x64" }, "sha512-EmhBsp+xuKb6YbZNEMLBo80Xw71AZOIm18do8ni/GLX/Lw1XzNhVYpbxO1KasYtoAvrGtCOdCPULRlIE0bPbTQ=="],
|
||||
|
||||
"@oxlint/binding-openharmony-arm64": ["@oxlint/binding-openharmony-arm64@1.88.0", "", { "os": "none", "cpu": "arm64" }, "sha512-DBUcx/KwYdg1DAz+G01P/GFv512J04EsZadbHptx2FTpWT/MWRg21ZToNHt9pnsfQRBQ3et/g+vqzbvrdAVbuA=="],
|
||||
|
||||
"@oxlint/binding-win32-arm64-msvc": ["@oxlint/binding-win32-arm64-msvc@1.88.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-xeVd0n4XG5HvynxfYvXi6uB+JvZbVZataowPS8cYpMbYg9mwE4sSyElFQd4LVJFifq8bRLDSR18XgEildmcXAg=="],
|
||||
|
||||
"@oxlint/binding-win32-ia32-msvc": ["@oxlint/binding-win32-ia32-msvc@1.88.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-MA6iOw//amleaz2mVMsZrA7NHlEexjTrhIxSMFCXQi6IUuQO+3Od00GZhr2iCYw6RFzMW36jkKK/WHBXMsoQAw=="],
|
||||
|
||||
"@oxlint/binding-win32-x64-msvc": ["@oxlint/binding-win32-x64-msvc@1.88.0", "", { "os": "win32", "cpu": "x64" }, "sha512-3Sz6rVWe8Dy+ufsXurFN1GX2dX43F/lQMSLelK9gFjiba7nHvLlqlhR5cHlWVnCpSqUY1hN9pyCX9XToe6ve6g=="],
|
||||
|
||||
"@peculiar/asn1-schema": ["@peculiar/asn1-schema@2.8.0", "", { "dependencies": { "@peculiar/utils": "^2.0.2", "asn1js": "^3.0.10", "tslib": "^2.8.1" } }, "sha512-7YT0U/ze0tF2QOBbE15gKZwy5tvgGyLRiRHLzhlbOpf7BT032oBSd0haZqXn5W6l26WLlu3dyxzjM+2638/z2Q=="],
|
||||
|
||||
"@peculiar/json-schema": ["@peculiar/json-schema@1.1.12", "", { "dependencies": { "tslib": "^2.0.0" } }, "sha512-coUfuoMeIB7B8/NMekxaDzLhaYmp0HZNPEjYRm9goRou8UZIC3z21s0sL9AWoCw4EG876QyO3kYrc61WNF9B/w=="],
|
||||
@@ -3452,6 +3491,8 @@
|
||||
|
||||
"own-keys": ["own-keys@1.0.1", "", { "dependencies": { "get-intrinsic": "^1.2.6", "object-keys": "^1.1.1", "safe-push-apply": "^1.0.0" } }, "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg=="],
|
||||
|
||||
"oxlint": ["oxlint@1.88.0", "", { "optionalDependencies": { "@oxlint/binding-android-arm-eabi": "1.88.0", "@oxlint/binding-android-arm64": "1.88.0", "@oxlint/binding-darwin-arm64": "1.88.0", "@oxlint/binding-darwin-x64": "1.88.0", "@oxlint/binding-freebsd-x64": "1.88.0", "@oxlint/binding-linux-arm-gnueabihf": "1.88.0", "@oxlint/binding-linux-arm-musleabihf": "1.88.0", "@oxlint/binding-linux-arm64-gnu": "1.88.0", "@oxlint/binding-linux-arm64-musl": "1.88.0", "@oxlint/binding-linux-ppc64-gnu": "1.88.0", "@oxlint/binding-linux-riscv64-gnu": "1.88.0", "@oxlint/binding-linux-riscv64-musl": "1.88.0", "@oxlint/binding-linux-s390x-gnu": "1.88.0", "@oxlint/binding-linux-x64-gnu": "1.88.0", "@oxlint/binding-linux-x64-musl": "1.88.0", "@oxlint/binding-openharmony-arm64": "1.88.0", "@oxlint/binding-win32-arm64-msvc": "1.88.0", "@oxlint/binding-win32-ia32-msvc": "1.88.0", "@oxlint/binding-win32-x64-msvc": "1.88.0" }, "peerDependencies": { "oxlint-tsgolint": ">=7.0.2003", "vite-plus": "*" }, "optionalPeers": ["oxlint-tsgolint", "vite-plus"], "bin": { "oxlint": "bin/oxlint" } }, "sha512-uIcFgw8BmqvFRGzH5+r7d2zJMEexwNSrEmM6WQNnGh3VkO0XYTEFoyxyNE6tjKMUib9Cue4XJA2DoWruO5D6aw=="],
|
||||
|
||||
"p-cancelable": ["p-cancelable@2.1.1", "", {}, "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg=="],
|
||||
|
||||
"p-limit": ["p-limit@6.2.0", "", { "dependencies": { "yocto-queue": "^1.1.1" } }, "sha512-kuUqqHNUqoIWp/c467RI4X6mmyuojY5jGutNU0wVTmEOOfcuwLqyMVoAi9MKi2Ak+5i9+nhmrK4ufZE8069kHA=="],
|
||||
|
||||
@@ -77,6 +77,7 @@
|
||||
"build:site": "bun --cwd apps/site build",
|
||||
"preview:web": "vite preview --config apps/web/vite.config.ts --host 127.0.0.1 --port 4173",
|
||||
"preview:site": "bun --cwd apps/site preview",
|
||||
"lint": "oxlint --deny-warnings apps packages",
|
||||
"typecheck": "tsc -p tsconfig.json --noEmit",
|
||||
"typecheck:mobile": "bun run --cwd apps/mobile typecheck",
|
||||
"typecheck:ios-editor": "bun run --cwd apps/ios/EditorSource typecheck",
|
||||
@@ -199,6 +200,7 @@
|
||||
"fake-indexeddb": "^6.2.5",
|
||||
"google-auth-library": "^11.0.2",
|
||||
"linkedom": "0.18.13",
|
||||
"oxlint": "1.88.0",
|
||||
"tailwindcss": "^4.3.2",
|
||||
"tw-animate-css": "^1.4.0",
|
||||
"typescript": "6.0.3",
|
||||
|
||||
@@ -91,6 +91,7 @@ const escapeMarkdownLabel = (value: string) =>
|
||||
|
||||
/** http(s) URLs that are not already stored EdgeEver resources. */
|
||||
const isRemoteHttpUrl = (value: string) => {
|
||||
// oxlint-disable-next-line no-control-regex -- rejects URLs containing control characters or whitespace
|
||||
if (!value || /[\u0000-\u0020"'<>`]/.test(value)) return false;
|
||||
if (getResourceIdFromUrl(value)) return false;
|
||||
try {
|
||||
@@ -102,6 +103,7 @@ const isRemoteHttpUrl = (value: string) => {
|
||||
};
|
||||
|
||||
const isDetailsImageSrc = (value: string) => {
|
||||
// oxlint-disable-next-line no-control-regex -- rejects image sources containing control characters or whitespace
|
||||
if (!value || /[\u0000-\u0020"'<>`]/.test(value)) return false;
|
||||
if (value.startsWith("edgeever-resource://") || value.startsWith("edgeever-staged://")) return true;
|
||||
if (value.startsWith("/") && !value.startsWith("//")) return true;
|
||||
@@ -135,7 +137,7 @@ const remoteVideoFromSrc = (src: string): RemoteVideo | null => {
|
||||
} catch {
|
||||
segment = "";
|
||||
}
|
||||
if (!segment || segment.length > 120 || /[\\/\[\]()<>]/.test(segment)) segment = "";
|
||||
if (!segment || segment.length > 120 || /[\\/[\]()<>]/.test(segment)) segment = "";
|
||||
const filename = segment || "video";
|
||||
return {
|
||||
src: url,
|
||||
@@ -320,7 +322,7 @@ export const wrapDetailsContentHtml = (html: string): string => {
|
||||
const document = new DOMParser().parseFromString(html, "text/html");
|
||||
const blocks = [...document.querySelectorAll("details")].reverse();
|
||||
for (const details of blocks) {
|
||||
for (const video of [...details.querySelectorAll("video")]) {
|
||||
for (const video of Array.from(details.querySelectorAll("video"))) {
|
||||
if (video.closest("[data-type='edgeever-file-attachment'], [data-edgeever-video-player]")) continue;
|
||||
const parsed = remoteVideoFromSrc(video.getAttribute("src") || "");
|
||||
if (!parsed) {
|
||||
@@ -332,7 +334,7 @@ export const wrapDetailsContentHtml = (html: string): string => {
|
||||
if (details.querySelector(':scope > [data-type="detailsContent"]')) continue;
|
||||
const wrapper = document.createElement("div");
|
||||
wrapper.setAttribute("data-type", "detailsContent");
|
||||
for (const child of [...details.childNodes]) {
|
||||
for (const child of Array.from(details.childNodes)) {
|
||||
if (child.nodeName === "SUMMARY") continue;
|
||||
wrapper.appendChild(child);
|
||||
}
|
||||
@@ -367,7 +369,7 @@ export const flattenDetailsForLinearHtml = (root: HTMLElement) => {
|
||||
heading.appendChild(strong);
|
||||
replacement.appendChild(heading);
|
||||
}
|
||||
for (const child of [...details.childNodes]) {
|
||||
for (const child of Array.from(details.childNodes)) {
|
||||
if (child === summary) continue;
|
||||
if (child instanceof HTMLElement && child.getAttribute("data-type") === "detailsContent") {
|
||||
while (child.firstChild) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { DiagramNodeShape } from "./diagram";
|
||||
|
||||
export const visualTextUnits = (label: string) => Array.from(label).reduce(
|
||||
// oxlint-disable-next-line no-control-regex -- tests for characters outside Latin-1
|
||||
(total, character) => total + (/[^\u0000-\u00ff]/.test(character) ? 1 : 0.55),
|
||||
0,
|
||||
);
|
||||
|
||||
@@ -394,7 +394,7 @@ export const applyDiagramOperations = async (
|
||||
}
|
||||
if (current.kind === "architecture" && affectedBoundaryIds.size > 0) {
|
||||
const nodeById = new Map(next.nodes.map((node) => [node.id, node]));
|
||||
for (const boundaryId of [...affectedBoundaryIds]) {
|
||||
for (const boundaryId of Array.from(affectedBoundaryIds)) {
|
||||
let parentId = nodeById.get(boundaryId)?.parentId;
|
||||
while (parentId) {
|
||||
affectedBoundaryIds.add(parentId);
|
||||
|
||||
@@ -145,7 +145,7 @@ export const diagramDocumentToX6Cells = (
|
||||
} : {}),
|
||||
},
|
||||
...(mindVisual ? { underline: mindVisual.underline } : {}),
|
||||
...(architectureVisual?.attrs ?? {}),
|
||||
...architectureVisual?.attrs,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
@@ -510,6 +510,7 @@ export const resolveTheme = (background?: NoteImageBackground, theme?: NoteImage
|
||||
|
||||
export const buildImageExportBasename = (title: string, fallback: string) => {
|
||||
const sanitized = title
|
||||
// oxlint-disable-next-line no-control-regex -- replaces control characters in export filenames
|
||||
.replace(/[\u0000-\u001f<>:"/\\|?*]/g, "-")
|
||||
.replace(/\s+/g, " ")
|
||||
.replace(/[. ]+$/g, "")
|
||||
|
||||
@@ -69,7 +69,7 @@ export const posterPatchSchema = z.object({
|
||||
radius: z.number().min(0).max(4096).optional(),
|
||||
resourceId: z.string().min(1).max(160).regex(/^[\w-]+$/).optional(),
|
||||
crop: z.object({ x: z.number().min(0).max(1), y: z.number().min(0).max(1), width: z.number().min(0.01).max(1), height: z.number().min(0.01).max(1) }).strict().refine(crop => crop.x + crop.width <= 1 && crop.y + crop.height <= 1).nullable().optional(),
|
||||
path: z.string().min(1).max(100000).regex(/^[MmLlHhVvCcSsQqTtAaZz\d\s,.eE+\-]+$/).optional(),
|
||||
path: z.string().min(1).max(100000).regex(/^[MmLlHhVvCcSsQqTtAaZz\d\s,.eE+-]+$/).optional(),
|
||||
text: z.string().max(8000).optional(),
|
||||
fill: z.string().regex(/^#[0-9a-f]{6}$/i).optional(),
|
||||
fontSize: z.number().finite().min(4).max(8192).optional(),
|
||||
|
||||
@@ -129,7 +129,7 @@ const path = z.object({
|
||||
.string()
|
||||
.min(1)
|
||||
.max(100_000)
|
||||
.regex(/^[MmLlHhVvCcSsQqTtAaZz\d\s,.eE+\-]+$/),
|
||||
.regex(/^[MmLlHhVvCcSsQqTtAaZz\d\s,.eE+-]+$/),
|
||||
});
|
||||
export const PosterElementSchema = z.union([text, shape, image, path]);
|
||||
export const PosterDocumentSchema = z
|
||||
@@ -294,7 +294,7 @@ export const posterFallbackMarkdown = (document: PosterDocument) =>
|
||||
: "",
|
||||
...document.elements
|
||||
.filter((element): element is PosterText => element.type === "text")
|
||||
.map((element) => element.text.replace(/([\\`*_{}\[\]<>#!])/g, "\\$1")),
|
||||
.map((element) => element.text.replace(/([\\`*_{}[\]<>#!])/g, "\\$1")),
|
||||
document.sourceMemoId ? `[Source](#memo=${document.sourceMemoId})` : "",
|
||||
...document.elements
|
||||
.filter((element) => element.type === "image")
|
||||
|
||||
@@ -348,6 +348,7 @@ export const AiDefaultModelUpdateSchema = z.object({
|
||||
});
|
||||
|
||||
export const AiAttachmentSchema = z.object({
|
||||
// oxlint-disable-next-line no-control-regex -- rejects control characters in attachment filenames
|
||||
filename: z.string().trim().min(1).max(255).regex(/^[^\u0000-\u001F\u007F]+$/),
|
||||
mediaType: z.enum(AI_ATTACHMENT_MEDIA_TYPES),
|
||||
base64Data: z.string().min(1).max(Math.ceil(MAX_AI_ATTACHMENT_BYTES / 3) * 4),
|
||||
|
||||
Reference in new issue
Block a user