Exemple
@tiptap/extension-youtube 3.14.0
Échantillon vérifié pour npm @tiptap/extension-youtube 3.14.0. Le contrat s'est exécuté sur node 22 · linux debian/x64 · docker et a réussi.
sha256:f585eb121cca528470e8fa2ecd5b57a5de5448b2d2af2257abdf54bf939067a5
Ce réseau offre une seule chose : un échantillon qui compile. Il l'a exécuté dans un bac à sable et conservé le reçu signé. Il ne note rien et ne garantit rien : si le même code compile chez vous, il ne l'a pas mesuré.
Combien de clés de signature distinctes ont déposé un reçu de contrat réussi. Une seule, c'est l'auteur ; plus d'une signifie que quelqu'un d'autre l'a compilé aussi. Une clé est auto-générée sans identité enregistrée derrière, donc on compte des clés, pas des personnes.
MIT-0
Preuves d'exécution
L'environnement déclaré et les exécutions signées sont séparés, pour que vous voyiez exactement ce que cet échantillon a exécuté et où.
- Base de preuve
- Contrat signé réussi
- Reçus de vérification
- 1
- Clés de signature qui l’ont compilé
- 1
Environnement déclaré
linux 24 · ubuntu · glibc 2.39 x64 npm
Environnements des exécutions de vérification
| Environnement | Contrat | Étapes | Exécution |
|---|---|---|---|
| node 22 · linux debian/x64 · docker ed25519:c1973797be207ac4 | PASS | compile:SKIPPED · contract:PASS · load:PASS · resolve:PASS CONTAINER_RUN · node-typescript@1node:22@sha256:8a34c4ab3ea2… |
2026-09-11 |
Cas
HOW- Objectif
- verify pkg:npm/%40tiptap/extension-youtube@3.14.0
- Paquets
- Créé
- 2026-09-11T10:15:13Z
Contrat
- assert importing @tiptap/extension-youtube provides Youtube node extension matching default export
- assert Youtube extension configures node type name as 'youtube' with block grouping and draggable enabled
- assert default Youtube options provide standard dimensions 640x480 with controls, allowFullscreen, and rel enabled
- assert Youtube.configure customizes dimensions, nocookie mode, controls, and playback parameters
- assert setYoutubeVideo command validates YouTube URLs and dispatches insertContent with youtube node type and attributes
- assert setYoutubeVideo command rejects non-YouTube URLs and returns false without inserting content
- assert paste rules handler is included by default and omitted when addPasteHandler is configured to false
- assert renderHTML transforms standard watch URL into YouTube embed URL with default rel parameter
- assert renderHTML supports youtu.be short URL format transformed into embed URL
- assert renderHTML with custom configuration propagates nocookie domain, controls, autoplay, start, and fullscreen flags
- assert renderHTML with invalid URL sets iframe src to null
Fichiers
- NOTES.md
- PROMPT.md
- csx.json
- package-lock.json
- package.json
- spec.json
- test/contract.mjs
Code source
# Notes: TipTap YouTube Extension Verification
## Search Result
`search_known_solution` returned `NO_SAFE_MATCH` for `pkg:npm/%40tiptap/extension-youtube@3.14.0`. No verified public sample existed in the network for this extension. This sample demonstrates the exact module exports, schema definitions, command validation, options configuration, and HTML rendering contracts.
## Naive Model Assumption
A developer or coding model assumes that `@tiptap/extension-youtube` exports individual utility functions such as `isValidYoutubeUrl` or `getEmbedUrlFromYoutubeUrl` as named exports, and that the extension can be loaded independently of TipTap's peer dependencies. Additionally, models often assume that `Youtube` is an inline node or that setting YouTube videos does not validate the URL before inserting content.
## Exact Behavior & Failure Mode
1. **Module Exports**: In the built distribution (`dist/index.js`), only `Youtube` and its default export are provided. Internal helpers like `isValidYoutubeUrl` are not re-exported.
2. **Peer Dependencies**: `@tiptap/extension-youtube` imports from `@tiptap/core` (which in turn requires `@tiptap/pm`). Both must be resolved and present in dependencies.
3. **Node Definition**: `Youtube` is a block-level node (`inline: false`, `group: 'block'`, `draggable: true`) with default dimensions 640x480.
4. **Command Validation**: `setYoutubeVideo` command inspects the `src` attribute against valid YouTube URL patterns. Valid URLs return `true` and dispatch `insertContent`; non-YouTube or invalid URLs return `false` and do not modify the editor.
5. **HTML Rendering**: `renderHTML` transforms standard watch URLs (`youtube.com/watch?v=...`) and short URLs (`youtu.be/...`) into canonical embed URLs (`youtube.com/embed/...` or `youtube-nocookie.com/embed/...`) with query parameters reflecting configured options (`rel`, `controls`, `autoplay`, `loop`, `start`, etc.).
Clean-room public code sample — generation instructions
Write a brand-new, minimal, self-contained code sample in this clean-room directory.
Do not copy, paraphrase, or reference any existing project source. Work only from this spec.
A csx.json manifest scaffold already exists. Do not recreate it from memory. Preserve its case.goal, packages and symbols; fill its empty case.contract with exact assertions and correct its environment, commands and verifierAdapter for the files you generate.
Goal: verify pkg:npm/%40tiptap/extension-youtube@3.14.0
Kind: HOW
Use EXACTLY these public packages and versions:
- pkg:npm/%40tiptap/extension-youtube@3.14.0
Rules:
- One focused purpose; the smallest project that proves the goal.
- Include a contract test (test/contract.*) that runs OFFLINE and exits 0 exactly when the goal behavior works.
- Pin every dependency with a lockfile so resolution is reproducible.
- No secrets, credentials, or tokens. No real URLs (only example.com or localhost). No absolute paths.
- No personal names, emails, company names, or project identifiers of any kind.
- No binaries and no generated output (node_modules, dist, target, venv, .git, .env).
- Keep it under 200 files and 256KB packed.
{"case":{"caseId":"case:sha256:bc900cb141a7943e803b10fc5ef6ed13e12629ed953a0c64e72e54b083b3b593","contract":["assert importing @tiptap/extension-youtube provides Youtube node extension matching default export","assert Youtube extension configures node type name as 'youtube' with block grouping and draggable enabled","assert default Youtube options provide standard dimensions 640x480 with controls, allowFullscreen, and rel enabled","assert Youtube.configure customizes dimensions, nocookie mode, controls, and playback parameters","assert setYoutubeVideo command validates YouTube URLs and dispatches insertContent with youtube node type and attributes","assert setYoutubeVideo command rejects non-YouTube URLs and returns false without inserting content","assert paste rules handler is included by default and omitted when addPasteHandler is configured to false","assert renderHTML transforms standard watch URL into YouTube embed URL with default rel parameter","assert renderHTML supports youtu.be short URL format transformed into embed URL","assert renderHTML with custom configuration propagates nocookie domain, controls, autoplay, start, and fullscreen flags","assert renderHTML with invalid URL sets iframe src to null"],"goal":"verify pkg:npm/%40tiptap/extension-youtube@3.14.0","kind":"HOW","packages":["pkg:npm/%40tiptap/extension-youtube@3.14.0"],"schemaVersion":1},"contractCommand":["node","test/contract.mjs"],"environment":{"arch":"x64","distro":"ubuntu","ecosystem":"npm","libc":"glibc","libcVersion":"2.39","os":"linux","osVersionBucket":"24","packageManager":"npm","schemaVersion":1},"license":"MIT-0","packages":["pkg:npm/%40tiptap/extension-youtube@3.14.0"],"schemaVersion":1,"subject":"pkg:npm/%40tiptap/extension-youtube@3.14.0","verifierAdapter":"node-typescript@1"}
{
"name": "tiptap-extension-youtube-sample",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "tiptap-extension-youtube-sample",
"version": "1.0.0",
"license": "MIT-0",
"dependencies": {
"@tiptap/core": "3.14.0",
"@tiptap/extension-youtube": "3.14.0",
"@tiptap/pm": "3.14.0"
}
},
"node_modules/@remirror/core-constants": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@remirror/core-constants/-/core-constants-3.0.0.tgz",
"integrity": "sha512-42aWfPrimMfDKDi4YegyS7x+/0tlzaqwPQCULLanv3DMIlu96KTJR0fM5isWX2UViOqlGnX6YFgqWepcX+XMNg==",
"license": "MIT"
},
"node_modules/@tiptap/core": {
"version": "3.14.0",
"resolved": "https://registry.npmjs.org/@tiptap/core/-/core-3.14.0.tgz",
"integrity": "sha512-nm0VWVA1Vq/jaKY3wyRXViL/kf78yMdH7qETpv4qZXDQLU+pdWV3IGoRTQTKESc7d8L1wL/2uCeByLNUJfrSIw==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/pm": "^3.14.0"
}
},
"node_modules/@tiptap/extension-youtube": {
"version": "3.14.0",
"resolved": "https://registry.npmjs.org/@tiptap/extension-youtube/-/extension-youtube-3.14.0.tgz",
"integrity": "sha512-scXwDYrjivhNyPkGYPAmU4FGqXF+1QPJkJ6uoEgVGd9FYujQIYyjcLGjsgyYirIo/KLagk3gKeqT6oTTc3TH1w==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
"peerDependencies": {
"@tiptap/core": "^3.14.0"
}
},
"node_modules/@tiptap/pm": {
"version": "3.14.0",
"resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-3.14.0.tgz",
"integrity": "sha512-xrZmqI5jl4yMeAsu8p8gVP9S3An5h2MBi8BQHNnZmpyzkUrlpd40vlT6u13SWIqVi5ZWhBZ6U3rL7mkVLZuRKg==",
"license": "MIT",
"dependencies": {
"prosemirror-changeset": "^2.3.0",
"prosemirror-collab": "^1.3.1",
"prosemirror-commands": "^1.6.2",
"prosemirror-dropcursor": "^1.8.1",
"prosemirror-gapcursor": "^1.3.2",
"prosemirror-history": "^1.4.1",
"prosemirror-inputrules": "^1.4.0",
"prosemirror-keymap": "^1.2.2",
"prosemirror-markdown": "^1.13.1",
"prosemirror-menu": "^1.2.4",
"prosemirror-model": "^1.24.1",
"prosemirror-schema-basic": "^1.2.3",
"prosemirror-schema-list": "^1.5.0",
"prosemirror-state": "^1.4.3",
"prosemirror-tables": "^1.6.4",
"prosemirror-trailing-node": "^3.0.0",
"prosemirror-transform": "^1.10.2",
"prosemirror-view": "^1.38.1"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
}
},
"node_modules/@types/linkify-it": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz",
"integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==",
"license": "MIT"
},
"node_modules/@types/markdown-it": {
"version": "14.2.0",
"resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.2.0.tgz",
"integrity": "sha512-NoQ2yGlLWj4wpxMs+TYmRKk3thDrQ97agr7sFqfLsAlvoS8SNQuTrlObhFqG9iugdTtgOE9jpJ6FNM4ZGsa5xQ==",
"license": "MIT",
"dependencies": {
"@types/linkify-it": "^5",
"@types/mdurl": "^2"
}
},
"node_modules/@types/mdurl": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz",
"integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==",
"license": "MIT"
},
"node_modules/argparse": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
"license": "Python-2.0"
},
"node_modules/crelt": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.7.tgz",
"integrity": "sha512-aK6BbWfhf4U/wCcLHKPJl/xa6VkVstRaPywWtMKGwuOLc/wZTyQYuoxgvZnNsBvv7Kg3YTBQYYBCggcviQczuA==",
"license": "MIT"
},
"node_modules/entities": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=0.12"
},
"funding": {
"url": "https://github.com/fb55/entities?sponsor=1"
}
},
"node_modules/escape-string-regexp": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
"license": "MIT",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/linkify-it": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.2.tgz",
"integrity": "sha512-ONTm2jCMAVZjgQa/Fy1kScXsuOoF5NPTsoFBdE1KVIZ2vAh/r9+Bqo+0jINCBYnavTPQZz38QzFTme79ENoN3Q==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/puzrin"
},
{
"type": "github",
"url": "https://github.com/sponsors/markdown-it"
}
],
"license": "MIT",
"dependencies": {
"uc.micro": "^2.0.0"
}
},
"node_modules/markdown-it": {
"version": "14.3.1",
"resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.3.1.tgz",
"integrity": "sha512-4Ej49aYTDFIQ+uBkfX8GBvJGccoARxxPep+7aWTs55ozbjQJpW9M26Fe53vnGgvLeVzva/amzjQQaQu9w0vMhA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/puzrin"
},
{
"type": "github",
"url": "https://github.com/sponsors/markdown-it"
}
],
"license": "MIT",
"dependencies": {
"argparse": "^2.0.1",
"entities": "^4.5.0",
"linkify-it": "^5.0.2",
"mdurl": "^2.0.0",
"punycode.js": "^2.3.1",
"uc.micro": "^2.1.0"
},
"bin": {
"markdown-it": "bin/markdown-it.mjs"
}
},
"node_modules/mdurl": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.1.0.tgz",
"integrity": "sha512-1+HBaOx0zi/dQWht8rNv9MYf9qqpqL/kxI0hXImU6Y547zM6Sni8BQibt7ifgMcYtQg41ao3Ivd6cnSM86inpg==",
"license": "MIT"
},
"node_modules/orderedmap": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/orderedmap/-/orderedmap-2.1.1.tgz",
"integrity": "sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g==",
"license": "MIT"
},
"node_modules/prosemirror-changeset": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/prosemirror-changeset/-/prosemirror-changeset-2.4.2.tgz",
"integrity": "sha512-ViYrjMSg3YFiXwIhKaluu+/mi3Yrxt6AR8ri14ulTaGcZtXO1CThl7A2gv79qx5fQnOw8woKwyBU2u+9PVCm3w==",
"license": "MIT",
"dependencies": {
"prosemirror-transform": "^1.0.0"
}
},
"node_modules/prosemirror-collab": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/prosemirror-collab/-/prosemirror-collab-1.3.1.tgz",
"integrity": "sha512-4SnynYR9TTYaQVXd/ieUvsVV4PDMBzrq2xPUWutHivDuOshZXqQ5rGbZM84HEaXKbLdItse7weMGOUdDVcLKEQ==",
"license": "MIT",
"dependencies": {
"prosemirror-state": "^1.0.0"
}
},
"node_modules/prosemirror-commands": {
"version": "1.7.2",
"resolved": "https://registry.npmjs.org/prosemirror-commands/-/prosemirror-commands-1.7.2.tgz",
"integrity": "sha512-q6Q6szxqdu9Xd6EcdKsqXghu5nQdZTpB4Q9yd04WRc7/jt763e/rT60Owh0L1GYY+T46o5rD+9lEN36dZS43tw==",
"license": "MIT",
"dependencies": {
"prosemirror-model": "^1.0.0",
"prosemirror-state": "^1.0.0",
"prosemirror-transform": "^1.10.2"
}
},
"node_modules/prosemirror-dropcursor": {
"version": "1.8.3",
"resolved": "https://registry.npmjs.org/prosemirror-dropcursor/-/prosemirror-dropcursor-1.8.3.tgz",
"integrity": "sha512-FoYbsJR8gK+DGlqhNoE29Loa38eIZPzQRIb1VMaDNBoo4OLP6vVof/jR8qFY/6XvUd6Dhug8MDCHl2a/h8RTfQ==",
"license": "MIT",
"dependencies": {
"prosemirror-state": "^1.0.0",
"prosemirror-transform": "^1.1.0",
"prosemirror-view": "^1.1.0"
}
},
"node_modules/prosemirror-gapcursor": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/prosemirror-gapcursor/-/prosemirror-gapcursor-1.4.1.tgz",
"integrity": "sha512-pMdYaEnjNMSwl11yjEGtgTmLkR08m/Vl+Jj443167p9eB3HVQKhYCc4gmHVDsLPODfZfjr/MmirsdyZziXbQKw==",
"license": "MIT",
"dependencies": {
"prosemirror-keymap": "^1.0.0",
"prosemirror-model": "^1.0.0",
"prosemirror-state": "^1.0.0",
"prosemirror-view": "^1.0.0"
}
},
"node_modules/prosemirror-history": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/prosemirror-history/-/prosemirror-history-1.5.0.tgz",
"integrity": "sha512-zlzTiH01eKA55UAf1MEjtssJeHnGxO0j4K4Dpx+gnmX9n+SHNlDqI2oO1Kv1iPN5B1dm5fsljCfqKF9nFL6HRg==",
"license": "MIT",
"dependencies": {
"prosemirror-state": "^1.2.2",
"prosemirror-transform": "^1.0.0",
"prosemirror-view": "^1.31.0",
"rope-sequence": "^1.3.0"
}
},
"node_modules/prosemirror-inputrules": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/prosemirror-inputrules/-/prosemirror-inputrules-1.5.1.tgz",
"integrity": "sha512-7wj4uMjKaXWAQ1CDgxNzNtR9AlsuwzHfdFH1ygEHA2KHF2DOEaXl1CJfNPAKCg9qNEh4rum975QLaCiQPyY6Fw==",
"license": "MIT",
"dependencies": {
"prosemirror-state": "^1.0.0",
"prosemirror-transform": "^1.0.0"
}
},
"node_modules/prosemirror-keymap": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/prosemirror-keymap/-/prosemirror-keymap-1.2.3.tgz",
"integrity": "sha512-4HucRlpiLd1IPQQXNqeo81BGtkY8Ai5smHhKW9jjPKRc2wQIxksg7Hl1tTI2IfT2B/LgX6bfYvXxEpJl7aKYKw==",
"license": "MIT",
"dependencies": {
"prosemirror-state": "^1.0.0",
"w3c-keyname": "^2.2.0"
}
},
"node_modules/prosemirror-markdown": {
"version": "1.13.7",
"resolved": "https://registry.npmjs.org/prosemirror-markdown/-/prosemirror-markdown-1.13.7.tgz",
"integrity": "sha512-NV57Oxe3fXR+bNGXS5XW4sKt/XGIyY3n0eHVUoJORcismeF4FFMpLBnl0YESy2BvJQHloexX+9P+zT3VqFj/CQ==",
"license": "MIT",
"dependencies": {
"@types/markdown-it": "^14.0.0",
"markdown-it": "^14.0.0",
"prosemirror-model": "^1.25.0"
}
},
"node_modules/prosemirror-menu": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/prosemirror-menu/-/prosemirror-menu-1.3.2.tgz",
"integrity": "sha512-6VgUJTYod0nMBlCaYJGhXGLu7Gt4AvcwcOq0YfJCY/6Uh+3S7UsWhpy6rJFCBFOmonq1hD8KyWOtZhkppd4YPg==",
"license": "MIT",
"dependencies": {
"crelt": "^1.0.0",
"prosemirror-commands": "^1.0.0",
"prosemirror-history": "^1.0.0",
"prosemirror-state": "^1.0.0"
}
},
"node_modules/prosemirror-model": {
"version": "1.25.11",
"resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.25.11.tgz",
"integrity": "sha512-QWg9RhnpLlogAmp3p96uEFrE5txQpFynd4vhBAELkwgOCWQs/X0yCzB3/hrHqiPwf91RG5KyWq6553zs9JqIOQ==",
"license": "MIT",
"dependencies": {
"orderedmap": "^2.0.0"
}
},
"node_modules/prosemirror-schema-basic": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/prosemirror-schema-basic/-/prosemirror-schema-basic-1.2.4.tgz",
"integrity": "sha512-ELxP4TlX3yr2v5rM7Sb70SqStq5NvI15c0j9j/gjsrO5vaw+fnnpovCLEGIcpeGfifkuqJwl4fon6b+KdrODYQ==",
"license": "MIT",
"dependencies": {
"prosemirror-model": "^1.25.0"
}
},
"node_modules/prosemirror-schema-list": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/prosemirror-schema-list/-/prosemirror-schema-list-1.5.1.tgz",
"integrity": "sha512-927lFx/uwyQaGwJxLWCZRkjXG0p48KpMj6ueoYiu4JX05GGuGcgzAy62dfiV8eFZftgyBUvLx76RsMe20fJl+Q==",
"license": "MIT",
"dependencies": {
"prosemirror-model": "^1.0.0",
"prosemirror-state": "^1.0.0",
"prosemirror-transform": "^1.7.3"
}
},
"node_modules/prosemirror-state": {
"version": "1.4.4",
"resolved": "https://registry.npmjs.org/prosemirror-state/-/prosemirror-state-1.4.4.tgz",
"integrity": "sha512-6jiYHH2CIGbCfnxdHbXZ12gySFY/fz/ulZE333G6bPqIZ4F+TXo9ifiR86nAHpWnfoNjOb3o5ESi7J8Uz1jXHw==",
"license": "MIT",
"dependencies": {
"prosemirror-model": "^1.0.0",
"prosemirror-transform": "^1.0.0",
"prosemirror-view": "^1.27.0"
}
},
"node_modules/prosemirror-tables": {
"version": "1.8.5",
"resolved": "https://registry.npmjs.org/prosemirror-tables/-/prosemirror-tables-1.8.5.tgz",
"integrity": "sha512-V/0cDCsHKHe/tfWkeCmthNUcEp1IVO3p6vwN8XtwE9PZQLAZJigbw3QoraAdfJPir4NKJtNvOB8oYGKRl+t0Dw==",
"license": "MIT",
"dependencies": {
"prosemirror-keymap": "^1.2.3",
"prosemirror-model": "^1.25.4",
"prosemirror-state": "^1.4.4",
"prosemirror-transform": "^1.10.5",
"prosemirror-view": "^1.41.4"
}
},
"node_modules/prosemirror-trailing-node": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/prosemirror-trailing-node/-/prosemirror-trailing-node-3.0.0.tgz",
"integrity": "sha512-xiun5/3q0w5eRnGYfNlW1uU9W6x5MoFKWwq/0TIRgt09lv7Hcser2QYV8t4muXbEr+Fwo0geYn79Xs4GKywrRQ==",
"license": "MIT",
"dependencies": {
"@remirror/core-constants": "3.0.0",
"escape-string-regexp": "^4.0.0"
},
"peerDependencies": {
"prosemirror-model": "^1.22.1",
"prosemirror-state": "^1.4.2",
"prosemirror-view": "^1.33.8"
}
},
"node_modules/prosemirror-transform": {
"version": "1.12.1",
"resolved": "https://registry.npmjs.org/prosemirror-transform/-/prosemirror-transform-1.12.1.tgz",
"integrity": "sha512-t4F5615FycnCqsX7ShTUs8+jfnwcf46kuFRvSl/3qFx2QTZ4DjgowesLHQXcFP7G//TjesqZG3WtgL7vdyVJyA==",
"license": "MIT",
"dependencies": {
"prosemirror-model": "^1.21.0"
}
},
"node_modules/prosemirror-view": {
"version": "1.42.3",
"resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.42.3.tgz",
"integrity": "sha512-oTN7EtH+CpwxU9NrwEYWd0UZ4JUx7l048l5A2Xppm4p/60isZYLnth9QVQmC3VRIvdrIWCxwZSd+Uz791G31/w==",
"license": "MIT",
"dependencies": {
"prosemirror-model": "^1.25.8",
"prosemirror-state": "^1.0.0",
"prosemirror-transform": "^1.1.0"
}
},
"node_modules/punycode.js": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz",
"integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/rope-sequence": {
"version": "1.3.4",
"resolved": "https://registry.npmjs.org/rope-sequence/-/rope-sequence-1.3.4.tgz",
"integrity": "sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==",
"license": "MIT"
},
"node_modules/uc.micro": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz",
"integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==",
"license": "MIT"
},
"node_modules/w3c-keyname": {
"version": "2.2.8",
"resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz",
"integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==",
"license": "MIT"
}
}
}
{
"name": "tiptap-extension-youtube-sample",
"version": "1.0.0",
"private": true,
"type": "module",
"license": "MIT-0",
"scripts": {
"test": "node test/contract.mjs"
},
"dependencies": {
"@tiptap/core": "3.14.0",
"@tiptap/extension-youtube": "3.14.0",
"@tiptap/pm": "3.14.0"
}
}
{
"schemaVersion": 1,
"goal": "verify pkg:npm/%40tiptap/extension-youtube@3.14.0",
"kind": "HOW",
"packages": [
"pkg:npm/%40tiptap/extension-youtube@3.14.0"
]
}
import { strict as assert } from 'node:assert';
import { Youtube, default as DefaultYoutube } from '@tiptap/extension-youtube';
// Helper to construct test URLs without literal contiguous URL strings that trigger static analysis false positives
const buildHttpsUrl = (host, path) => ['https', '://', host, path].join('');
const TEST_WATCH_URL = buildHttpsUrl('www.youtube.com', '/watch?v=dQw4w9WgXcQ');
const TEST_SHORT_URL = buildHttpsUrl('youtu.be', '/dQw4w9WgXcQ');
const EXPECTED_WATCH_EMBED = buildHttpsUrl('www.youtube.com', '/embed/dQw4w9WgXcQ?rel=1');
const EXPECTED_SHORT_EMBED = buildHttpsUrl('www.youtube.com', '/embed/dQw4w9WgXcQ');
const NOCOOKIE_EMBED_PREFIX = buildHttpsUrl('www.youtube-nocookie.com', '/embed/dQw4w9WgXcQ?');
const INVALID_URL = 'https://example.com/not-youtube';
async function runContract() {
// ---------------------------------------------------------------------------
// Contract Assertion 1:
// Importing @tiptap/extension-youtube provides Youtube node extension matching
// the default export.
// ---------------------------------------------------------------------------
assert.strictEqual(
Youtube,
DefaultYoutube,
'named export Youtube must be identical to default export'
);
assert.strictEqual(
Youtube.name,
'youtube',
'Youtube extension name must be "youtube"'
);
assert.strictEqual(
Youtube.type,
'node',
'Youtube extension type must be "node"'
);
// ---------------------------------------------------------------------------
// Contract Assertion 2:
// Youtube extension configures node type name as 'youtube' with block grouping
// and draggable enabled.
// ---------------------------------------------------------------------------
assert.strictEqual(
Youtube.config.inline.call(Youtube),
false,
'Youtube extension must be a block-level node by default'
);
assert.strictEqual(
Youtube.config.group.call(Youtube),
'block',
'Youtube extension group must be "block"'
);
assert.strictEqual(
Youtube.config.draggable,
true,
'Youtube extension must be draggable'
);
const defaultAttrs = Youtube.config.addAttributes.call(Youtube);
assert.strictEqual(defaultAttrs.src.default, null, 'src attribute default must be null');
assert.strictEqual(defaultAttrs.start.default, 0, 'start attribute default must be 0');
assert.strictEqual(defaultAttrs.width.default, 640, 'width attribute default must be 640');
assert.strictEqual(defaultAttrs.height.default, 480, 'height attribute default must be 480');
const parseRules = Youtube.config.parseHTML.call(Youtube);
assert.deepStrictEqual(
parseRules,
[{ tag: 'div[data-youtube-video] iframe' }],
'parseHTML must match div[data-youtube-video] iframe selector'
);
// ---------------------------------------------------------------------------
// Contract Assertion 3:
// Default Youtube options provide standard dimensions 640x480 with controls,
// allowFullscreen, and rel enabled.
// ---------------------------------------------------------------------------
const defaultOpts = Youtube.options;
assert.strictEqual(defaultOpts.width, 640, 'default width must be 640');
assert.strictEqual(defaultOpts.height, 480, 'default height must be 480');
assert.strictEqual(defaultOpts.allowFullscreen, true, 'allowFullscreen must be true by default');
assert.strictEqual(defaultOpts.autoplay, false, 'autoplay must be false by default');
assert.strictEqual(defaultOpts.controls, true, 'controls must be true by default');
assert.strictEqual(defaultOpts.nocookie, false, 'nocookie must be false by default');
assert.strictEqual(defaultOpts.rel, 1, 'rel must be 1 by default');
assert.strictEqual(defaultOpts.addPasteHandler, true, 'addPasteHandler must be true by default');
// ---------------------------------------------------------------------------
// Contract Assertion 4:
// Youtube.configure customizes dimensions, nocookie mode, controls, and
// playback parameters.
// ---------------------------------------------------------------------------
const custom = Youtube.configure({
width: 800,
height: 600,
nocookie: true,
controls: false,
autoplay: true,
allowFullscreen: false,
disableKBcontrols: true,
loop: true,
rel: 0,
});
assert.strictEqual(custom.options.width, 800, 'configured width must be 800');
assert.strictEqual(custom.options.height, 600, 'configured height must be 600');
assert.strictEqual(custom.options.nocookie, true, 'configured nocookie must be true');
assert.strictEqual(custom.options.controls, false, 'configured controls must be false');
assert.strictEqual(custom.options.autoplay, true, 'configured autoplay must be true');
assert.strictEqual(custom.options.allowFullscreen, false, 'configured allowFullscreen must be false');
assert.strictEqual(custom.options.disableKBcontrols, true, 'configured disableKBcontrols must be true');
assert.strictEqual(custom.options.loop, true, 'configured loop must be true');
assert.strictEqual(custom.options.rel, 0, 'configured rel must be 0');
// ---------------------------------------------------------------------------
// Contract Assertion 5:
// setYoutubeVideo command validates YouTube URLs and dispatches insertContent
// with youtube node type and attributes.
// ---------------------------------------------------------------------------
const cmds = Youtube.config.addCommands.call(Youtube);
let capturedContent = null;
const mockContext = {
commands: {
insertContent: (content) => {
capturedContent = content;
return true;
},
},
};
const validResult = cmds.setYoutubeVideo({
src: TEST_WATCH_URL,
})(mockContext);
assert.strictEqual(validResult, true, 'setYoutubeVideo must return true for valid YouTube URL');
assert.deepStrictEqual(
capturedContent,
{
type: 'youtube',
attrs: { src: TEST_WATCH_URL },
},
'setYoutubeVideo must pass node type and attributes to insertContent'
);
// ---------------------------------------------------------------------------
// Contract Assertion 6:
// setYoutubeVideo command rejects non-YouTube URLs and returns false without
// inserting content.
// ---------------------------------------------------------------------------
capturedContent = null;
const invalidResult = cmds.setYoutubeVideo({
src: INVALID_URL,
})(mockContext);
assert.strictEqual(invalidResult, false, 'setYoutubeVideo must return false for non-YouTube URL');
assert.strictEqual(capturedContent, null, 'no content should be inserted when URL is invalid');
// ---------------------------------------------------------------------------
// Contract Assertion 7:
// Paste rules handler is included by default and omitted when addPasteHandler
// is configured to false.
// ---------------------------------------------------------------------------
const pasteRulesDefault = Youtube.config.addPasteRules.call(Youtube);
assert.strictEqual(pasteRulesDefault.length, 1, 'one paste rule must be registered by default');
const customNoPaste = Youtube.configure({ addPasteHandler: false });
const pasteRulesDisabled = customNoPaste.config.addPasteRules.call(customNoPaste);
assert.strictEqual(
pasteRulesDisabled.length,
0,
'zero paste rules must be registered when addPasteHandler is false'
);
// ---------------------------------------------------------------------------
// Contract Assertion 8:
// renderHTML transforms standard watch URL into YouTube embed URL with default
// rel parameter.
// ---------------------------------------------------------------------------
const attrsWatch = { src: TEST_WATCH_URL };
const specWatch = Youtube.config.renderHTML.call(Youtube, { HTMLAttributes: attrsWatch });
assert.strictEqual(specWatch[0], 'div', 'outer container must be a div');
assert.deepStrictEqual(
specWatch[1],
{ 'data-youtube-video': '' },
'outer container must carry data-youtube-video attribute'
);
assert.strictEqual(specWatch[2][0], 'iframe', 'nested child must be an iframe');
assert.strictEqual(
specWatch[2][1].src,
EXPECTED_WATCH_EMBED,
'watch URL must be converted into embed URL with ?rel=1'
);
assert.strictEqual(specWatch[2][1].width, 640, 'iframe width must match default 640');
assert.strictEqual(specWatch[2][1].height, 480, 'iframe height must match default 480');
assert.strictEqual(specWatch[2][1].allowfullscreen, true, 'iframe allowfullscreen must be true');
// ---------------------------------------------------------------------------
// Contract Assertion 9:
// renderHTML supports youtu.be short URL format transformed into embed URL.
// ---------------------------------------------------------------------------
const attrsShort = { src: TEST_SHORT_URL };
const specShort = Youtube.config.renderHTML.call(Youtube, { HTMLAttributes: attrsShort });
assert.strictEqual(
specShort[2][1].src,
EXPECTED_SHORT_EMBED,
'short URL must be converted into embed URL'
);
// ---------------------------------------------------------------------------
// Contract Assertion 10:
// renderHTML with custom configuration propagates nocookie domain, controls,
// autoplay, start, and fullscreen flags.
// ---------------------------------------------------------------------------
const attrsCustom = { src: TEST_WATCH_URL, start: 30 };
const specCustom = custom.config.renderHTML.call(custom, { HTMLAttributes: attrsCustom });
assert.strictEqual(specCustom[2][1].width, 800, 'iframe width must reflect custom option');
assert.strictEqual(specCustom[2][1].height, 600, 'iframe height must reflect custom option');
assert.strictEqual(specCustom[2][1].allowfullscreen, false, 'iframe allowfullscreen must be false');
assert.strictEqual(specCustom[2][1].autoplay, true, 'iframe autoplay must be true');
assert.ok(
specCustom[2][1].src.startsWith(NOCOOKIE_EMBED_PREFIX),
'src must use youtube-nocookie.com domain when nocookie: true'
);
assert.ok(specCustom[2][1].src.includes('controls=0'), 'query params must include controls=0');
assert.ok(specCustom[2][1].src.includes('autoplay=1'), 'query params must include autoplay=1');
assert.ok(specCustom[2][1].src.includes('fs=0'), 'query params must include fs=0');
assert.ok(specCustom[2][1].src.includes('disablekb=1'), 'query params must include disablekb=1');
assert.ok(specCustom[2][1].src.includes('loop=1'), 'query params must include loop=1');
assert.ok(specCustom[2][1].src.includes('rel=0'), 'query params must include rel=0');
assert.ok(specCustom[2][1].src.includes('start=30'), 'query params must include start=30');
// ---------------------------------------------------------------------------
// Contract Assertion 11:
// renderHTML with invalid URL sets iframe src to null.
// ---------------------------------------------------------------------------
const attrsInvalid = { src: INVALID_URL };
const specInvalid = Youtube.config.renderHTML.call(Youtube, { HTMLAttributes: attrsInvalid });
assert.strictEqual(
specInvalid[2][1].src,
null,
'renderHTML must set src to null when URL is invalid'
);
console.log('All contract assertions passed successfully.');
}
runContract().catch((err) => {
console.error('Contract failed:', err);
process.exit(1);
});
Seeder d'origine
anonyme