샘플
@tiptap/extension-youtube 3.14.0
검증된 샘플 — npm @tiptap/extension-youtube 3.14.0. node 22 · linux debian/x64 · docker에서 contract를 실행해 통과했습니다: assert importing @tiptap/extension-youtube…
sha256:f585eb121cca528470e8fa2ecd5b57a5de5448b2d2af2257abdf54bf939067a5
이 네트워크가 제공하는 것은 하나입니다. 빌드되는 샘플. 샌드박스에서 돌리고 서명된 영수증을 보관합니다. 등급을 매기지 않고 무엇도 보증하지 않습니다 — 같은 코드가 당신 환경에서 빌드되는지는 측정한 적이 없습니다.
통과한 계약 영수증을 낸 서로 다른 서명 키의 수입니다. 하나면 작성자 혼자이고, 둘 이상이면 다른 사람도 빌드했다는 뜻입니다. 키는 스스로 만드는 것이고 뒤에 등록된 신원이 없으므로, 세는 것은 사람이 아니라 키입니다.
MIT-0
실행 증거
선언된 환경과 서명된 실행을 분리해 두었습니다. 이 샘플이 무엇을 어디서 실행했는지 그대로 볼 수 있습니다.
- 증거 기준
- 서명된 컨트랙트 통과
- 검증 영수증
- 1
- 빌드한 서명 키
- 1
선언된 환경
linux 24 · ubuntu · glibc 2.39 x64 npm
검증 실행 환경
| 환경 | 컨트랙트 | 단계 | 실행일 |
|---|---|---|---|
| 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 |
케이스
HOW- 목표
- verify pkg:npm/%40tiptap/extension-youtube@3.14.0
- 생성일
- 2026-09-11T10:15:13Z
컨트랙트
- 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
파일
- NOTES.md
- PROMPT.md
- csx.json
- package-lock.json
- package.json
- spec.json
- test/contract.mjs
소스
# 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);
});
오리진 시더
익명