Skip to content

Commit 15eab07

Browse files
fix: link HTML attributes (BLO-915) (#2687)
1 parent d48a92a commit 15eab07

83 files changed

Lines changed: 373 additions & 326 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

packages/core/src/extensions/tiptap-extensions/Link/link.ts

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,6 @@ import { UNICODE_WHITESPACE_REGEX_GLOBAL } from "./helpers/whitespace.js";
1010

1111
const DEFAULT_PROTOCOL = "https";
1212

13-
const HTML_ATTRIBUTES = {
14-
target: "_blank",
15-
rel: "noopener noreferrer nofollow",
16-
className: "bn-inline-content-section",
17-
"data-inline-content-type": "link",
18-
};
19-
2013
// Pre-compiled regex for URI protocol validation.
2114
// Allows: http, https, ftp, ftps, mailto, tel, callto, sms, cid, xmpp
2215
const ALLOWED_URI_REGEX =
@@ -84,7 +77,12 @@ export const Link = Mark.create<LinkOptions>({
8477

8578
addOptions() {
8679
return {
87-
HTMLAttributes: {},
80+
HTMLAttributes: {
81+
target: "_blank",
82+
rel: "noopener noreferrer nofollow",
83+
className: "bn-inline-content-section",
84+
"data-inline-content-type": "link",
85+
},
8886
editor: undefined,
8987
onClick: undefined,
9088
isValidLink: isAllowedUri,
@@ -99,12 +97,6 @@ export const Link = Mark.create<LinkOptions>({
9997
return element.getAttribute("href");
10098
},
10199
},
102-
target: {
103-
default: HTML_ATTRIBUTES.target,
104-
},
105-
rel: {
106-
default: HTML_ATTRIBUTES.rel,
107-
},
108100
};
109101
},
110102

@@ -128,12 +120,22 @@ export const Link = Mark.create<LinkOptions>({
128120
if (!this.options.isValidLink(HTMLAttributes.href)) {
129121
return [
130122
"a",
131-
mergeAttributes(HTML_ATTRIBUTES, { ...HTMLAttributes, href: "" }),
123+
mergeAttributes(
124+
{
125+
...HTMLAttributes,
126+
href: "",
127+
},
128+
this.options.HTMLAttributes,
129+
),
132130
0,
133131
];
134132
}
135133

136-
return ["a", mergeAttributes(HTML_ATTRIBUTES, HTMLAttributes), 0];
134+
return [
135+
"a",
136+
mergeAttributes(HTMLAttributes, this.options.HTMLAttributes),
137+
0,
138+
];
137139
},
138140

139141
addPasteRules() {

packages/xl-ai/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@
6666
"lint": "eslint src --max-warnings 0",
6767
"test": "NODE_EXTRA_CA_CERTS=\"$(mkcert -CAROOT)/rootCA.pem\" vitest --run",
6868
"test-watch": "NODE_EXTRA_CA_CERTS=\"$(mkcert -CAROOT)/rootCA.pem\" vitest watch",
69+
"rename-msw-snapshots": "node scripts/rename-msw-snapshots.mjs",
6970
"email": "email dev"
7071
},
7172
"dependencies": {
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
#!/usr/bin/env node
2+
// Repair msw-snapshot files after a request-shape change.
3+
//
4+
// When the request body changes (e.g. a schema change in BlockNote alters the
5+
// HTML/JSON sent to the LLM), the md5 hash that msw-snapshot embeds in each
6+
// cached response filename no longer matches. msw-snapshot then treats the
7+
// snapshot as missing and (because of `updateSnapshots: "missing"`) falls
8+
// through to the real API, which fails in CI without credentials and writes a
9+
// new file at the *correct* new hash containing the failure response (e.g.
10+
// 401).
11+
//
12+
// After that failed run, every affected slot has two files:
13+
// <test>_<seq>_<old-hash>.json -- valid 200 response, wrong hash
14+
// <test>_<seq>_<new-hash>.json -- right hash, but a 401 body
15+
//
16+
// This script transplants the 200 response into the new-hash file and deletes
17+
// the old-hash file, leaving exactly one file per slot with the right hash
18+
// and the right response.
19+
//
20+
// Usage:
21+
// pnpm --filter @blocknote/xl-ai test # populates the new-hash files
22+
// pnpm --filter @blocknote/xl-ai rename-msw-snapshots
23+
// pnpm --filter @blocknote/xl-ai test # all green
24+
import {
25+
readFileSync,
26+
readdirSync,
27+
statSync,
28+
unlinkSync,
29+
writeFileSync,
30+
} from "node:fs";
31+
import path from "node:path";
32+
import { fileURLToPath } from "node:url";
33+
34+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
35+
const PKG_ROOT = path.resolve(__dirname, "..");
36+
const SEARCH_ROOT = path.join(PKG_ROOT, "src");
37+
38+
const FILE_RE = /^(.+)_(\d+)_([a-f0-9]+)\.json$/;
39+
40+
function* walk(dir) {
41+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
42+
const p = path.join(dir, entry.name);
43+
if (entry.isDirectory()) {
44+
yield* walk(p);
45+
} else if (entry.isFile() && entry.name.endsWith(".json")) {
46+
yield p;
47+
}
48+
}
49+
}
50+
51+
const filesByDir = new Map();
52+
for (const file of walk(SEARCH_ROOT)) {
53+
if (!file.includes(`${path.sep}__msw_snapshots__${path.sep}`)) continue;
54+
const dir = path.dirname(file);
55+
const list = filesByDir.get(dir) ?? [];
56+
list.push(path.basename(file));
57+
filesByDir.set(dir, list);
58+
}
59+
60+
let migrated = 0;
61+
let skipped = 0;
62+
const skipNotes = [];
63+
64+
for (const [dir, files] of filesByDir) {
65+
const groups = new Map();
66+
for (const file of files) {
67+
const match = FILE_RE.exec(file);
68+
if (!match) continue;
69+
const slot = `${match[1]}_${match[2]}`;
70+
const list = groups.get(slot) ?? [];
71+
list.push(file);
72+
groups.set(slot, list);
73+
}
74+
75+
for (const [slot, group] of groups) {
76+
if (group.length < 2) continue;
77+
78+
const entries = group.map((file) => {
79+
const fp = path.join(dir, file);
80+
const data = JSON.parse(readFileSync(fp, "utf8"));
81+
return {
82+
file,
83+
path: fp,
84+
data,
85+
status: data?.response?.status,
86+
mtime: statSync(fp).mtimeMs,
87+
};
88+
});
89+
90+
const good = entries.filter((e) => e.status === 200);
91+
const bad = entries.filter((e) => e.status !== 200);
92+
93+
if (good.length !== 1 || bad.length === 0) {
94+
skipped++;
95+
skipNotes.push(
96+
` ${path.relative(PKG_ROOT, dir)}/${slot}: ${good.length} good + ${bad.length} bad`,
97+
);
98+
continue;
99+
}
100+
101+
// Use the most recently written bad file as the destination — its hash
102+
// matches the current request body.
103+
bad.sort((a, b) => b.mtime - a.mtime);
104+
const target = bad[0];
105+
106+
target.data.response = good[0].data.response;
107+
writeFileSync(target.path, JSON.stringify(target.data, null, 2));
108+
unlinkSync(good[0].path);
109+
for (const extra of bad.slice(1)) unlinkSync(extra.path);
110+
111+
migrated++;
112+
console.log(
113+
`migrated ${path.relative(PKG_ROOT, dir)}/${slot} -> ${target.file}`,
114+
);
115+
}
116+
}
117+
118+
console.log(`\nDone. ${migrated} migrated, ${skipped} skipped.`);
119+
if (skipped > 0) {
120+
console.log("\nSkipped slots (need manual attention):");
121+
for (const note of skipNotes) console.log(note);
122+
}
123+
if (migrated === 0 && skipped === 0) {
124+
console.log(
125+
"\nNo mismatched snapshot pairs found. If you expected some, run\n" +
126+
"`pnpm --filter @blocknote/xl-ai test` first to let msw-snapshot record\n" +
127+
"the new-hash files alongside the existing old-hash ones.",
128+
);
129+
}

packages/xl-ai/src/api/formats/html-blocks/__snapshots__/htmlBlocks.test.ts/Combined/__msw_snapshots__/anthropic.messages/claude-3-7-sonnet-latest (streaming)/add and update paragraph_1_801ad86e0c3a4562338793805e66a52f.json renamed to packages/xl-ai/src/api/formats/html-blocks/__snapshots__/htmlBlocks.test.ts/Combined/__msw_snapshots__/anthropic.messages/claude-3-7-sonnet-latest (streaming)/add and update paragraph_1_0acceced44cd885ee4439d4ef8639ba2.json

File renamed without changes.

packages/xl-ai/src/api/formats/html-blocks/__snapshots__/htmlBlocks.test.ts/Combined/__msw_snapshots__/anthropic.messages/claude-3-7-sonnet-latest (streaming)/add paragraph and update selection_1_b6ecf36636295d284db3ba7243cd4835.json renamed to packages/xl-ai/src/api/formats/html-blocks/__snapshots__/htmlBlocks.test.ts/Combined/__msw_snapshots__/anthropic.messages/claude-3-7-sonnet-latest (streaming)/add paragraph and update selection_1_9f552939a63a167da4dd5b1abffbfc43.json

File renamed without changes.

packages/xl-ai/src/api/formats/html-blocks/__snapshots__/htmlBlocks.test.ts/Combined/__msw_snapshots__/groq.chat/llama-3.3-70b-versatile (streaming)/add and update paragraph_1_3c276441275032fe98b12356026537a0.json renamed to packages/xl-ai/src/api/formats/html-blocks/__snapshots__/htmlBlocks.test.ts/Combined/__msw_snapshots__/groq.chat/llama-3.3-70b-versatile (streaming)/add and update paragraph_1_f5ce14780d38e0a320dc0246f9fa3fa7.json

File renamed without changes.

packages/xl-ai/src/api/formats/html-blocks/__snapshots__/htmlBlocks.test.ts/Combined/__msw_snapshots__/groq.chat/llama-3.3-70b-versatile (streaming)/add paragraph and update selection_1_0207de852025d2c0a1d417a7d66fa03c.json renamed to packages/xl-ai/src/api/formats/html-blocks/__snapshots__/htmlBlocks.test.ts/Combined/__msw_snapshots__/groq.chat/llama-3.3-70b-versatile (streaming)/add paragraph and update selection_1_8157654938ade250b36e1dde3bf0a5c2.json

File renamed without changes.

packages/xl-ai/src/api/formats/html-blocks/__snapshots__/htmlBlocks.test.ts/Combined/__msw_snapshots__/openai.responses/gpt-4o-2024-08-06 (streaming)/add and update paragraph_1_937647a13580b4dbb611e3de3b2c8788.json renamed to packages/xl-ai/src/api/formats/html-blocks/__snapshots__/htmlBlocks.test.ts/Combined/__msw_snapshots__/openai.responses/gpt-4o-2024-08-06 (streaming)/add and update paragraph_1_eb11dd6643fc4f655a9d55dbd71b19b9.json

File renamed without changes.

packages/xl-ai/src/api/formats/html-blocks/__snapshots__/htmlBlocks.test.ts/Combined/__msw_snapshots__/openai.responses/gpt-4o-2024-08-06 (streaming)/add paragraph and update selection_1_d9ea724851130649f405ff50190452b5.json renamed to packages/xl-ai/src/api/formats/html-blocks/__snapshots__/htmlBlocks.test.ts/Combined/__msw_snapshots__/openai.responses/gpt-4o-2024-08-06 (streaming)/add paragraph and update selection_1_bcb205a92155b4f033637eb9f5fb2d3b.json

File renamed without changes.

packages/xl-ai/src/api/formats/html-blocks/__snapshots__/htmlBlocks.test.ts/Delete/__msw_snapshots__/anthropic.messages/claude-3-7-sonnet-latest (streaming)/delete first block_1_afd5ee1bda075c7482d1861d03ad0a29.json renamed to packages/xl-ai/src/api/formats/html-blocks/__snapshots__/htmlBlocks.test.ts/Delete/__msw_snapshots__/anthropic.messages/claude-3-7-sonnet-latest (streaming)/delete first block_1_de1c4968c1329dacd5d81bcd2b3ed8d1.json

File renamed without changes.

0 commit comments

Comments
 (0)