diff --git a/package.json b/package.json index ba39ad4c..732d2bf3 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "test": "vitest run", "test:watch": "vitest", "test:e2e": "playwright test", - "playwright:smoke": "playwright test --config playwright.smoke.config.ts tests/smoke/example.spec.ts tests/smoke/fix-ai-chat-empty-body-v3.spec.ts tests/smoke/fix-crash-create-note.spec.ts tests/smoke/fresh-start-telemetry-onboarding.spec.ts tests/smoke/getting-started-template.spec.ts tests/smoke/h1-title-decoupled.spec.ts tests/smoke/h1-untitled-auto-rename.spec.ts tests/smoke/keyboard-command-routing.spec.ts tests/smoke/multi-selection-shortcuts.spec.ts tests/smoke/wikilink-path-fix.spec.ts", + "playwright:smoke": "playwright test --config playwright.smoke.config.ts tests/smoke/example.spec.ts tests/smoke/fix-ai-chat-empty-body-v3.spec.ts tests/smoke/fix-crash-create-note.spec.ts tests/smoke/fresh-start-telemetry-onboarding.spec.ts tests/smoke/getting-started-template.spec.ts tests/smoke/h1-title-decoupled.spec.ts tests/smoke/h1-untitled-auto-rename.spec.ts tests/smoke/keyboard-command-routing.spec.ts tests/smoke/linkify-init-warnings.spec.ts tests/smoke/multi-selection-shortcuts.spec.ts tests/smoke/wikilink-path-fix.spec.ts", "playwright:regression": "playwright test tests/smoke/", "playwright:integration": "playwright test --config playwright.integration.config.ts", "test:coverage": "node scripts/run-vitest-coverage.mjs", diff --git a/patches/@blocknote__core@0.46.2.patch b/patches/@blocknote__core@0.46.2.patch index d5c0133a..a81ba7d9 100644 --- a/patches/@blocknote__core@0.46.2.patch +++ b/patches/@blocknote__core@0.46.2.patch @@ -11,3 +11,50 @@ index b2761001278486a8b2ac1d10c82420b4994e96d9..fbf4f2f450ed1351d64adeb16263ceac if (l === u) return r.dispatch(r.state.tr.insertText(i)), r.dispatch( r.state.tr.setMeta(B, { +diff --git a/src/editor/managers/ExtensionManager/extensions.ts b/src/editor/managers/ExtensionManager/extensions.ts +--- a/src/editor/managers/ExtensionManager/extensions.ts ++++ b/src/editor/managers/ExtensionManager/extensions.ts +@@ -84,7 +84,8 @@ export function getDefaultTiptapExtensions( + }).configure({ + defaultProtocol: DEFAULT_LINK_PROTOCOL, + // only call this once if we have multiple editors installed. Or fix https://github.com/ueberdosis/tiptap/issues/5450 +- protocols: LINKIFY_INITIALIZED ? [] : VALID_LINK_PROTOCOLS, ++ // Tolaria pre-registers BlockNote's non-native protocols before linkify initializes. ++ protocols: [], + }), + ...(Object.values(editor.schema.styleSpecs).map((styleSpec) => { + return styleSpec.implementation.mark.configure({ +diff --git a/dist/blocknote.js b/dist/blocknote.js +--- a/dist/blocknote.js ++++ b/dist/blocknote.js +@@ -2037,7 +2037,9 @@ const de = () => { + ] + }) + ); +-let fe = !1; ++const bnLinkifyProtocolFlag = "__tolariaBlockNoteLinkifyProtocolsRegistered"; ++const bnGlobalObject = typeof globalThis > "u" ? void 0 : globalThis; ++let fe = Boolean(bnGlobalObject && bnGlobalObject[bnLinkifyProtocolFlag]); + function mn(o, e) { + const t = [ + I.ClipboardTextSerializer, +@@ -2063,8 +2065,9 @@ function mn(o, e) { + inclusive: !1 + }).configure({ + defaultProtocol: Ct, + // only call this once if we have multiple editors installed. Or fix https://github.com/ueberdosis/tiptap/issues/5450 +- protocols: fe ? [] : Bt ++ // Tolaria pre-registers BlockNote's non-native protocols before linkify initializes. ++ protocols: [] + }), + ...Object.values(o.schema.styleSpecs).map((n) => n.implementation.mark.configure({ + editor: o +@@ -2112,7 +2115,7 @@ function mn(o, e) { + ), + Do(o) + ]; +- return fe = !0, t; ++ return fe = !0, bnGlobalObject && (bnGlobalObject[bnLinkifyProtocolFlag] = !0), t; + } + function kn(o, e) { + const t = [ diff --git a/patches/@tiptap__extension-link@3.19.0.patch b/patches/@tiptap__extension-link@3.19.0.patch new file mode 100644 index 00000000..66d84c82 --- /dev/null +++ b/patches/@tiptap__extension-link@3.19.0.patch @@ -0,0 +1,98 @@ +diff --git a/src/link.ts b/src/link.ts +--- a/src/link.ts ++++ b/src/link.ts +@@ -6,6 +6,21 @@ import { autolink } from './helpers/autolink.js' + import { clickHandler } from './helpers/clickHandler.js' + import { pasteHandler } from './helpers/pasteHandler.js' + import { UNICODE_WHITESPACE_REGEX_GLOBAL } from './helpers/whitespace.js' ++ ++const PRE_REGISTERED_PROTOCOLS = ['tel', 'callto', 'sms', 'cid', 'xmpp'] as const ++const linkifyProtocolFlag = '__tolariaTiptapLinkifyProtocolsRegistered' ++const linkifyGlobalObject = typeof globalThis === 'undefined' ++ ? undefined ++ : (globalThis as Record) ++ ++if (linkifyGlobalObject && !linkifyGlobalObject[linkifyProtocolFlag]) { ++ PRE_REGISTERED_PROTOCOLS.forEach((protocol) => { ++ registerCustomProtocol(protocol) ++ }) ++ ++ linkifyGlobalObject[linkifyProtocolFlag] = true ++} + + export interface LinkProtocolOptions { + /** +diff --git a/dist/index.js b/dist/index.js +--- a/dist/index.js ++++ b/dist/index.js +@@ -8,6 +8,18 @@ var UNICODE_WHITESPACE_REGEX = new RegExp(UNICODE_WHITESPACE_PATTERN); + var UNICODE_WHITESPACE_REGEX_END = new RegExp(`${UNICODE_WHITESPACE_PATTERN}$`); + var UNICODE_WHITESPACE_REGEX_GLOBAL = new RegExp(UNICODE_WHITESPACE_PATTERN, "g"); + ++var PRE_REGISTERED_PROTOCOLS = ["tel", "callto", "sms", "cid", "xmpp"]; ++var linkifyProtocolFlag = "__tolariaTiptapLinkifyProtocolsRegistered"; ++var linkifyGlobalObject = typeof globalThis === "undefined" ? void 0 : globalThis; ++if (linkifyGlobalObject && !linkifyGlobalObject[linkifyProtocolFlag]) { ++ PRE_REGISTERED_PROTOCOLS.forEach((protocol) => { ++ registerCustomProtocol(protocol); ++ }); ++ linkifyGlobalObject[linkifyProtocolFlag] = true; ++} ++ + // src/helpers/autolink.ts + function isValidLinkStructure(tokens) { + if (tokens.length === 1) { +@@ -224,7 +236,8 @@ var Link = Mark.create({ + }); + }, + onDestroy() { +- reset(); ++ // Tolaria keeps a single editor shell alive across note swaps, so linkify's ++ // protocol registry must survive editor teardown and remount cycles. + }, + inclusive() { + return this.options.autolink; +@@ -443,4 +444,4 @@ export { + isAllowedUri, + pasteRegex + }; +-//# sourceMappingURL=index.js.map +\ No newline at end of file ++//# sourceMappingURL=index.js.map +diff --git a/dist/index.cjs b/dist/index.cjs +--- a/dist/index.cjs ++++ b/dist/index.cjs +@@ -10,6 +10,18 @@ var import_state3 = require("@tiptap/pm/state"); + var import_linkifyjs2 = require("linkifyjs"); + var import_whitespace = require("./helpers/whitespace.cjs"); + ++var PRE_REGISTERED_PROTOCOLS = ["tel", "callto", "sms", "cid", "xmpp"]; ++var linkifyProtocolFlag = "__tolariaTiptapLinkifyProtocolsRegistered"; ++var linkifyGlobalObject = typeof globalThis === "undefined" ? void 0 : globalThis; ++if (linkifyGlobalObject && !linkifyGlobalObject[linkifyProtocolFlag]) { ++ PRE_REGISTERED_PROTOCOLS.forEach((protocol) => { ++ (0, import_linkifyjs.registerCustomProtocol)(protocol); ++ }); ++ linkifyGlobalObject[linkifyProtocolFlag] = true; ++} ++ + // src/helpers/autolink.ts + function isValidLinkStructure(tokens) { + if (tokens.length === 1) { +@@ -253,7 +265,8 @@ var Link = import_core3.Mark.create({ + }); + }, + onDestroy() { +- (0, import_linkifyjs3.reset)(); ++ // Tolaria keeps a single editor shell alive across note swaps, so linkify's ++ // protocol registry must survive editor teardown and remount cycles. + }, + inclusive() { + return this.options.autolink; +@@ -472,4 +473,4 @@ var index_default = Link; + isAllowedUri, + pasteRegex + }); +-//# sourceMappingURL=index.cjs.map +\ No newline at end of file ++//# sourceMappingURL=index.cjs.map diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9b253c97..736cef1b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -6,11 +6,14 @@ settings: patchedDependencies: '@blocknote/core@0.46.2': - hash: 9a6d8a53058a8b6127326d40e8188f6b132b9b9e364b87e88e1c9aa3a1867aec + hash: 04a4b91896adec69aba5d9fedd91f76f7b54aefad70e093cbc0a18f216829323 path: patches/@blocknote__core@0.46.2.patch '@blocknote/react@0.46.2': hash: e3726d75ed65e07adb1fd48262d6a15f8e52b1cc257177aaf632937830170c97 path: patches/@blocknote__react@0.46.2.patch + '@tiptap/extension-link@3.19.0': + hash: 9bc08d5f05c6bb2ca803ed64cec4c0cccfc6a6482147131488e3426324f587de + path: patches/@tiptap__extension-link@3.19.0.patch prosemirror-tables@1.8.5: hash: cd456f0d1d88a3ce11bcf53122a0e0575c0d82810eddd887adae21d6ec570a8b path: patches/prosemirror-tables@1.8.5.patch @@ -24,10 +27,10 @@ importers: version: 0.78.0(zod@4.3.6) '@blocknote/code-block': specifier: ^0.46.2 - version: 0.46.2(@blocknote/core@0.46.2(patch_hash=9a6d8a53058a8b6127326d40e8188f6b132b9b9e364b87e88e1c9aa3a1867aec)(@types/hast@3.0.4)(highlight.js@11.11.1)(lowlight@3.3.0)) + version: 0.46.2(@blocknote/core@0.46.2(patch_hash=04a4b91896adec69aba5d9fedd91f76f7b54aefad70e093cbc0a18f216829323)(@types/hast@3.0.4)(highlight.js@11.11.1)(lowlight@3.3.0)) '@blocknote/core': specifier: ^0.46.2 - version: 0.46.2(patch_hash=9a6d8a53058a8b6127326d40e8188f6b132b9b9e364b87e88e1c9aa3a1867aec)(@types/hast@3.0.4)(highlight.js@11.11.1)(lowlight@3.3.0) + version: 0.46.2(patch_hash=04a4b91896adec69aba5d9fedd91f76f7b54aefad70e093cbc0a18f216829323)(@types/hast@3.0.4)(highlight.js@11.11.1)(lowlight@3.3.0) '@blocknote/mantine': specifier: ^0.46.2 version: 0.46.2(@floating-ui/dom@1.7.5)(@mantine/core@8.3.14(@mantine/hooks@8.3.14(react@19.2.4))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@mantine/hooks@8.3.14(react@19.2.4))(@mantine/utils@6.0.22(react@19.2.4))(@types/hast@3.0.4)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(highlight.js@11.11.1)(lowlight@3.3.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) @@ -4511,9 +4514,9 @@ snapshots: '@bcoe/v8-coverage@1.0.2': {} - '@blocknote/code-block@0.46.2(@blocknote/core@0.46.2(patch_hash=9a6d8a53058a8b6127326d40e8188f6b132b9b9e364b87e88e1c9aa3a1867aec)(@types/hast@3.0.4)(highlight.js@11.11.1)(lowlight@3.3.0))': + '@blocknote/code-block@0.46.2(@blocknote/core@0.46.2(patch_hash=04a4b91896adec69aba5d9fedd91f76f7b54aefad70e093cbc0a18f216829323)(@types/hast@3.0.4)(highlight.js@11.11.1)(lowlight@3.3.0))': dependencies: - '@blocknote/core': 0.46.2(patch_hash=9a6d8a53058a8b6127326d40e8188f6b132b9b9e364b87e88e1c9aa3a1867aec)(@types/hast@3.0.4)(highlight.js@11.11.1)(lowlight@3.3.0) + '@blocknote/core': 0.46.2(patch_hash=04a4b91896adec69aba5d9fedd91f76f7b54aefad70e093cbc0a18f216829323)(@types/hast@3.0.4)(highlight.js@11.11.1)(lowlight@3.3.0) '@shikijs/core': 3.23.0 '@shikijs/engine-javascript': 3.23.0 '@shikijs/langs': 3.23.0 @@ -4521,7 +4524,7 @@ snapshots: '@shikijs/themes': 3.23.0 '@shikijs/types': 3.22.0 - '@blocknote/core@0.46.2(patch_hash=9a6d8a53058a8b6127326d40e8188f6b132b9b9e364b87e88e1c9aa3a1867aec)(@types/hast@3.0.4)(highlight.js@11.11.1)(lowlight@3.3.0)': + '@blocknote/core@0.46.2(patch_hash=04a4b91896adec69aba5d9fedd91f76f7b54aefad70e093cbc0a18f216829323)(@types/hast@3.0.4)(highlight.js@11.11.1)(lowlight@3.3.0)': dependencies: '@emoji-mart/data': 1.2.1 '@handlewithcare/prosemirror-inputrules': 0.1.4(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.6) @@ -4532,7 +4535,7 @@ snapshots: '@tiptap/extension-code': 3.19.0(@tiptap/core@3.19.0(@tiptap/pm@3.19.0)) '@tiptap/extension-horizontal-rule': 3.19.0(@tiptap/core@3.19.0(@tiptap/pm@3.19.0))(@tiptap/pm@3.19.0) '@tiptap/extension-italic': 3.19.0(@tiptap/core@3.19.0(@tiptap/pm@3.19.0)) - '@tiptap/extension-link': 3.19.0(@tiptap/core@3.19.0(@tiptap/pm@3.19.0))(@tiptap/pm@3.19.0) + '@tiptap/extension-link': 3.19.0(patch_hash=9bc08d5f05c6bb2ca803ed64cec4c0cccfc6a6482147131488e3426324f587de)(@tiptap/core@3.19.0(@tiptap/pm@3.19.0))(@tiptap/pm@3.19.0) '@tiptap/extension-paragraph': 3.19.0(@tiptap/core@3.19.0(@tiptap/pm@3.19.0)) '@tiptap/extension-strike': 3.19.0(@tiptap/core@3.19.0(@tiptap/pm@3.19.0)) '@tiptap/extension-text': 3.19.0(@tiptap/core@3.19.0(@tiptap/pm@3.19.0)) @@ -4573,7 +4576,7 @@ snapshots: '@blocknote/mantine@0.46.2(@floating-ui/dom@1.7.5)(@mantine/core@8.3.14(@mantine/hooks@8.3.14(react@19.2.4))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(@mantine/hooks@8.3.14(react@19.2.4))(@mantine/utils@6.0.22(react@19.2.4))(@types/hast@3.0.4)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(highlight.js@11.11.1)(lowlight@3.3.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: - '@blocknote/core': 0.46.2(patch_hash=9a6d8a53058a8b6127326d40e8188f6b132b9b9e364b87e88e1c9aa3a1867aec)(@types/hast@3.0.4)(highlight.js@11.11.1)(lowlight@3.3.0) + '@blocknote/core': 0.46.2(patch_hash=04a4b91896adec69aba5d9fedd91f76f7b54aefad70e093cbc0a18f216829323)(@types/hast@3.0.4)(highlight.js@11.11.1)(lowlight@3.3.0) '@blocknote/react': 0.46.2(patch_hash=e3726d75ed65e07adb1fd48262d6a15f8e52b1cc257177aaf632937830170c97)(@floating-ui/dom@1.7.5)(@types/hast@3.0.4)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(highlight.js@11.11.1)(lowlight@3.3.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@mantine/core': 8.3.14(@mantine/hooks@8.3.14(react@19.2.4))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@mantine/hooks': 8.3.14(react@19.2.4) @@ -4595,7 +4598,7 @@ snapshots: '@blocknote/react@0.46.2(patch_hash=e3726d75ed65e07adb1fd48262d6a15f8e52b1cc257177aaf632937830170c97)(@floating-ui/dom@1.7.5)(@types/hast@3.0.4)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(highlight.js@11.11.1)(lowlight@3.3.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': dependencies: - '@blocknote/core': 0.46.2(patch_hash=9a6d8a53058a8b6127326d40e8188f6b132b9b9e364b87e88e1c9aa3a1867aec)(@types/hast@3.0.4)(highlight.js@11.11.1)(lowlight@3.3.0) + '@blocknote/core': 0.46.2(patch_hash=04a4b91896adec69aba5d9fedd91f76f7b54aefad70e093cbc0a18f216829323)(@types/hast@3.0.4)(highlight.js@11.11.1)(lowlight@3.3.0) '@emoji-mart/data': 1.2.1 '@floating-ui/react': 0.27.17(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@floating-ui/utils': 0.2.10 @@ -6288,7 +6291,7 @@ snapshots: dependencies: '@tiptap/core': 3.19.0(@tiptap/pm@3.19.0) - '@tiptap/extension-link@3.19.0(@tiptap/core@3.19.0(@tiptap/pm@3.19.0))(@tiptap/pm@3.19.0)': + '@tiptap/extension-link@3.19.0(patch_hash=9bc08d5f05c6bb2ca803ed64cec4c0cccfc6a6482147131488e3426324f587de)(@tiptap/core@3.19.0(@tiptap/pm@3.19.0))(@tiptap/pm@3.19.0)': dependencies: '@tiptap/core': 3.19.0(@tiptap/pm@3.19.0) '@tiptap/pm': 3.19.0 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 1d327cd0..a65a6ca4 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -7,4 +7,5 @@ ignoredBuiltDependencies: patchedDependencies: '@blocknote/core@0.46.2': patches/@blocknote__core@0.46.2.patch '@blocknote/react@0.46.2': patches/@blocknote__react@0.46.2.patch + '@tiptap/extension-link@3.19.0': patches/@tiptap__extension-link@3.19.0.patch prosemirror-tables@1.8.5: patches/prosemirror-tables@1.8.5.patch diff --git a/tests/smoke/linkify-init-warnings.spec.ts b/tests/smoke/linkify-init-warnings.spec.ts new file mode 100644 index 00000000..9634535d --- /dev/null +++ b/tests/smoke/linkify-init-warnings.spec.ts @@ -0,0 +1,62 @@ +import { test, expect } from '@playwright/test' +import { + createFixtureVaultCopy, + openFixtureVaultDesktopHarness, + removeFixtureVaultCopy, +} from '../helpers/fixtureVault' + +let tempVaultDir: string + +function collectLinkifyWarnings(page: import('@playwright/test').Page) { + const warnings: string[] = [] + + page.on('console', (message) => { + const text = message.text() + if (/linkifyjs: already initialized - will not register custom scheme/i.test(text)) { + const location = message.location() + const locationLabel = [ + location.url || 'unknown-url', + location.lineNumber ?? 'unknown-line', + location.columnNumber ?? 'unknown-column', + ].join(':') + warnings.push(`${locationLabel} ${text}`) + } + }) + + return warnings +} + +async function openNote(page: import('@playwright/test').Page, title: string) { + await page.getByTestId('note-list-container').getByText(title, { exact: true }).click() + await expect(page.getByRole('heading', { name: title, level: 1 })).toBeVisible({ timeout: 5_000 }) +} + +test.beforeEach(async ({ page }, testInfo) => { + testInfo.setTimeout(60_000) + tempVaultDir = createFixtureVaultCopy() + await page.goto('/') + await page.waitForLoadState('networkidle') + await openFixtureVaultDesktopHarness(page, tempVaultDir) +}) + +test.afterEach(() => { + removeFixtureVaultCopy(tempVaultDir) +}) + +test('@smoke note open and editor remount flows stay free of duplicate linkify warnings', async ({ page }) => { + const linkifyWarnings = collectLinkifyWarnings(page) + + await openNote(page, 'Alpha Project') + await openNote(page, 'Note B') + + await page.keyboard.press('Control+Backslash') + await expect(page.getByTestId('raw-editor-codemirror')).toBeVisible({ timeout: 5_000 }) + + await page.keyboard.press('Control+Backslash') + await expect(page.locator('.bn-editor')).toBeVisible({ timeout: 5_000 }) + + await openNote(page, 'Alpha Project') + await page.waitForTimeout(300) + + expect(linkifyWarnings).toEqual([]) +})