You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Ship a sixth publishable Tiptap package, @docs.plus/extension-pull-quote. It adds one contentful wrap node, pullquote. The toolbar label is Pull quote.
This is a displayed, attributed quote. It can sit Left, Center, or Right, or wrap Left or Right, the same five placements as hypermultimedia.
It does not replace StarterKit blockquote. That node stays Quote. Indent, Markdown >, and Word Quote / Intense Quote keep using it.
Visual inspiration (look only, not tokens or markup):
Editorial: Testimonial 2 — serif quote, hairline, name and tagline.
Do not copy those components, their cn helper, or their tokens. Pad skins use house tokens. The published package CSS stays host-agnostic (light-dark() literals, class prefix pq-).
Why this name
Name
Ruling
Quote
Rejected. The pad toolbar, the composer Quote button, and the empty-blockquote placeholder already use that word.
Testimonial
Rejected for the first ship. It names customer praise and a verified mark. The node must also serve essays and reports.
Pull quote
Chosen. Editorial name for a displayed, attributed quote that can float beside body text.
A later rename needs a schema migration. Pick this name on the first insert.
What we have today
Five publishable packages, all 2.0.0:
Package
Job
extension-hyperlink
Link mark, autolink, host popovers
extension-hypermultimedia
Nine media nodes, captions, placement, media toolbar
extension-indent
Tab / Shift-Tab; default allowlist includes paragraph in blockquote
extension-inline-code
Inline code mark
extension-placeholder
Cursor-only empty hint (pad). Composer still uses the Tiptap built-in.
The pad also loads webapp-only extensions (TitleDocument, HeadingScale, HeadingFold, HeadingFilter, HeadingActions, ParagraphStyle, Highlight, MarkdownPaste, MediaUploadPlaceholder). The chat composer loads hyperlink, indent, and inline-code only. It does not load hypermultimedia.
blockquote is already on both editors. The pad paints it as a left bar. It has no author, source, avatar, or placement attrs.
Media captions are a caption attr plus a figcaption shell. They are not a quote node.
There is no attributed-quote, testimonial, pull-quote, or cite type in the schema or in CONTEXT.md.
Tiptap contract
Reviewed against Tiptap 3 (@tiptap/core 3.31.3) and official extension-blockquote at ueberdosis/tiptap 8623fbe. This node is a wrap, like Blockquote. It is not a media leaf and not a Details fence.
Flag
Value
Why
name
'pullquote'
Schema id. Same split as blockquote.
group
'block'
Required for TitleDocument heading block*.
content
'paragraph+'
Product cap. Official Blockquote is block+. This forbids list, heading, media, and nested pull quotes.
defining
true
Official wrap / paste-keep flag.
isolating
omit / false
ProseMirror liftTarget returns null on an isolating parent. unsetPullQuote would fail.
draggable
omit / false
First ship. Move the node with commands. Do not copy NodeView.onDragStart.
selectable
default (true)
Official default for a non-text node.
atom
omit / false
The body is typed content.
HTMLAttributes
{} in addOptions
Official Node option.
Official Figure (demos/…/Figure/Vue/figure.ts) is the inverse hole: content in figcaption, image from attrs. Do not copy contentElement: 'figcaption'. Pull quote puts content in blockquote and paints figcaption from attrs.
Official Details is definingandisolating because it has two child types and custom Enter / Backspace. Pull quote has one hole. Cite is attrs. Copy Blockquote, not Details, for schema flags.
Commands
Augment @tiptap/core with key pullQuote (camelCase), same split as official blockQuote / 'blockquote'. House surface file: typed PullQuotePublicCommands plus declare module, like hyperlink.
Command
Helper
Job
setPullQuote(attrs?)
commands.wrapIn(this.name, attrs)
Wrap the current paragraphs. An empty paragraph becomes an empty pull quote.
unsetPullQuote
commands.lift(this.name)
Lift children. Keep text.
updatePullQuote
commands.updateAttributes('pullquote', attrs)
Set cite attrs. Gate URLs.
setPullQuotePlacement
thin wrapper
One of the five placement ids. Writes display, float, clear, margin.
setPullQuoteVariant
thin wrapper
editorial or card.
No togglePullQuote. Official Blockquote has toggleWrap. Official Details is set / unset only. Do not fight toggleBlockquote.
Do not call insertContent for set. That helper replaces the selection. House media uses it because those nodes are leaves. wrapIn already wraps an empty paragraph and a paragraph range.
enableContentCheck is on. A node with no children fails .check(). wrapIn keeps the selected paragraphs. A fallback that builds JSON must include { type: 'paragraph' }. Prefer this.type.createAndFill() if a later path needs a vacant node. Do not insert { type: 'pullquote' } alone.
A selected heading cannot become a paragraph+ child. setPullQuote must not replace the title heading.
editor.can().setPullQuote() disables the toolbar when wrap cannot apply.
Keyboard and input rules
Copy official Blockquote Backspace only (handleBackspace.ts). Bind it to type pullquote.
Copy nothing else from Blockquote:
No wrappingInputRule for >. That stays Quote.
No Mod-Shift-b. That stays toggleBlockquote.
No Mod-Shift-9.
No custom Enter map. Empty-block lift is default liftEmptyBlock.
HTML
Discriminator: figure[data-pullquote]. Inner blockquote is the content hole. figcaption is painted chrome.
renderHTML has one0, and only inside blockquote. Never a second hole in figcaption.
Machine attrs emit data-* on the figure: data-author, data-role, data-href, data-src, data-variant, plus the four placement attrs. Each attr declares parseHTML / renderHTML. Do not rely on Tiptap’s default getAttribute('src') on the figure.
Cite fields never use innerHTML. Avatar renders as <img> only.
Default extension priority (100) is enough. The tag is unique. X uses priority: 101 because it fights generic blockquote. Do not add a generic figure rule. Do not add a blockquote rule.
Family harden in the same change: image getAttrs should take :scope > img, not any descendant img. A card avatar must not become an image node.
Node view
Vanilla addNodeView. Required. Not optional polish. No ReactNodeViewRenderer in the published package.
dom = the figure.
contentDOM = the inner blockquote.
Toolbar appends onto dom, sibling of contentDOM (media openMediaToolbar pattern). It must not appear in getHTML().
figcaption is contenteditable="false". No second caret island. Do not reuse createCaptionElement.
update() returns true for the same type and patches cite, variant, and placement in place.
ignoreMutation: official contentful default. selection and mutations inside contentDOM return false. Toolbar, figcaption, wrapper, and dom === target return true.
Do not call house ignoreNodeViewSubtreeMutation. That helper is for leaf media. It would swallow typing.
stopEvent: stop toolbar and figcaption only. Never stop the hole.
No aria-live / [role="status"] / output inside .ProseMirror.
Float CSS applies to dom, not to contentDOM.
Markdown
Hooks live on the node, same as hyperlink and image. They stay inert unless the host loads @tiptap/markdown.
First ship:
No markdownTokenName.
No parseMarkdown.
No markdownTokenizer.
renderMarkdown only, keyed by node name pullquote.
Copy official Blockquote.renderMarkdown for the body. Prefix each child line with >. Then append a cite line without>. Build [Name](href) in this handler from attrs. Blank an unsafe href.
Do not set markdownTokenName: 'blockquote'. That steals every > from StarterKit.
Do not spread createBlockMarkdownSpec on first ship. That helper emits :::pullquote and replaces the > export. Ship that grammar later if product wants a lossless hand-authored form. > import stays blockquote.
Do not use helpers.wrapInBlock('> ', …). Official Blockquote walks children itself.
Product Markdown export uses getMarkdownManager() over the migration set. A silent no-toDOM stub writes ''. Encode set imports the real@docs.plus/extension-pull-quote node, same as HyperMultimediaKit. Do not add toDOM on a storage stub.
What to build
One defining wrap node. The quote body is real paragraph+ content, so bold, italic, hyperlink, and inline code work.
Cite fields are attrs on the wrapper, not a second content hole:
author — display name
role — short tagline
href — source or profile link
src — optional avatar URL
variant — editorial (default) or card
placement: display, float, clear, margin (same four names as media)
Do not join HyperMultimediaKit. A pull quote is text with a cite, not a player. It must not gain grippers, Replace URL, Download, or the loading shell.
Do not import @docs.plus/extension-hypermultimedia for placement. Copy the small helper. A first-ship extract into a shared package is YAGNI. Do not import that package for caption or ignoreMutation either.
Placement
Same labels as the media Align submenu:
Label
Id
Effect
Left
inline
Block, no float
Center
center
margin: auto
Right
right
Push to the end
Wrap left
float-left
Text wraps on the right
Wrap right
float-right
Text wraps on the left
Wrap keeps the media margin presets. Default wrap gap is 0.5in. Floated boxes must stay inside the editor column (width plus wrap margins).
Wrap plus a typed contentDOM is the hard path. Media wrap works because media nodes are leaves. Schema stores all five placements from day one. Prove wrap in the browser (desktop and iOS) before the toolbar shows those two rows. If wrap fails, ship Left / Center / Right and keep the attrs.
No stored pixel width. No resize gripper. Wrap width is CSS (max-width on the figure). Do not use official ResizableNodeView.
Variants
One node. One variant attr.
Variant
Look
Needs
editorial
Serif quote, decorative marks, hairline, name and role
Body. Cite optional.
card
Bordered card, optional avatar, name, role
Body. Empty src hides the image.
The whole figure is not one link. Nested links fight contenteditable. The author name is the link when href is set.
No verified badge on first ship. Anyone who can edit the pad can tick a box. A check next to a name reads as platform identity. That is a lie.
Host hooks
Host-agnostic surfaces, same split as hyperlink and hypermultimedia:
The package ships a small prebuilt form so the playground and npm hosts work. Webapp replaces it with React.
Host may inject Comment through quoteActions. Comment on the body as a text anchor. Do not add this node to MediaNodeType. Jump must not key on avatar src. Do not stamp UniqueID on pullquote.
Family and pad seams
This is a new workspace member. The same change must:
Add it to scripts/publishable-extensions.ts (clean-room port 5178).
Add a key in .github/filters/extensions.yaml.
COPY the new package.json into all three Dockerfiles. Frozen lockfile fails without that. Hocuspocus also needs source plus a build, because the encode set will import the node.
Register the real node in apps/hocuspocus.server/src/lib/migration-extensions.ts before any client can insert. List every attr with a default. A missing stored type fails encode. It is not a flatten bug.
Configure it in pad TipTap.tsx only. Not in the chat composer.
Add a toolbar control next to Blockquote, data-testid="toolbar-pull-quote", with a distinct icon.
Add { textblock: 'paragraph', parent: 'pullquote' } to pad Indent only. Do not change the indent package default.
Add a CONTEXT.md glossary row so prose does not rotate names.
Keep family README / CHANGELOG / CONTRIBUTING parity. Bun-only install line. Gallery: hero plus editorial and card scenes.
Align @tiptap/core and @tiptap/pm with the family pin (3.31.3).
Do not stamp UniqueID on pullquote. It is not a TOC target.
Ship webapp from workspace:* first. Publish on the next family release. Do not cut a lone npm version.
Old tabs without the type can freeze or drop the subtree when a new tab inserts one. Deploy server schema, then pad insert. Long-lived tabs need a refresh.
Do not add pullquote to LEGACY_STRIP_BLOCK_PARENTS unless a real nested-heading corpus exists. paragraph+ makes that list the wrong home.
Leave markdownPastePlugin.ts weighting > as blockquote. Do not add a pull-quote detector for >.
URL gates
Name the link attr href and the avatar attr src. Paste and Settings import only rewrite those two names.
Author href: same scheme floor as hyperlink. http, https, and mailto only. Blank a bad value on parse, command, and render. Do not window.open a raw stored value.
Avatar src: same write floor as media. No blob: on write. No SVG data: image. Render as <img> only. Do not add an iframe host for an author site.
Quote text is ProseMirror content. Do not store HTML in an attr and paint it with innerHTML.
Placement attrs are an enum, not a raw style string.
Do not hang these fields on blockquote. An old schema drops unknown attrs on re-encode. A later save by an old tab would strip the cite and keep only the text.
Collaborative attrs are not re-validated on sync. Blank a bad href / src in renderHTML and in the node view. Use updateAttributes / setNodeMarkup. Never setContent to edit cite fields.
Markdown, HTML, and conversion
HTML copy must round-trip through figure[data-pullquote]. Clean-room Cypress covers getHTML() → setContent(), same rule as the media kit. Assert the toolbar is absent from getHTML().
Markdown is lossy on purpose. Export the body as > lines. A present author becomes a cite line outside the prefix (— Name, Role, optional link). Drop variant, placement, and src.
Import of > stays blockquote. Do not wait for #244 option blocks. Most media attrs already die on product Markdown export. A pull quote that needs those attrs to survive GFM is the wrong design.
DOCX / ODT: first ship may fall through to children. Word Quote styles stay blockquote. Do not fetch a third-party avatar during export. toPortableJson does not degrade this node (it is not an embed). Conversion must see real parseHTML / renderHTML.
Composer schema will drop a pasted pull quote. Accept that.
Package scaffold, node (defining, paragraph+, group: 'block'), URL gate, CSS, playground, vanilla NodeView, contentElement. Encode set imports the real node. No pad insert yet.
Commands (setPullQuote / unsetPullQuote / update*), Backspace helper, in-node toolbar hooks, HTML round-trip Cypress, javascript:href / src refused, > does not become pullquote. renderMarkdown emits > plus a cite line.
Pad: TipTap.tsx, toolbars, placeholder, indent rule, React attribution form and mobile sheet.
Browser proof: editorial + card, light and dark, all shown placements, wrap + typing beside the figure, iOS caret. Hide wrap in the toolbar if that path fails.
Slice 1 must land the encode-set registration before slice 4 turns on insert.
Acceptance criteria
Pad toolbar Pull quote wraps with setPullQuote. Blockquote still toggles blockquote.
Empty pull quote hint reads Pull quote. Empty blockquote hint still reads Quote.
User can type in the body. Marks work. Backspace matches official Blockquote lift. Enter stays default.
Attribution form sets author, role, href, and src. Figcaption updates. No second caret island.
Variant switches editorial and card without changing the node type.
Left, Center, and Right paint in the DOM, not only in attrs.
Wrap left and Wrap right stay inside the editor column, or those two rows stay hidden.
getHTML() → setContent() keeps the node, body, cite attrs, variant, and placement. getHTML() has no toolbar.
figure[data-hm-figure] stays image. blockquote.twitter-tweet stays X. Bare <blockquote> and > stay Quote. Avatar img does not become image.
Markdown export is > plus a cite line outside the prefix. Markdown import of > is still blockquote.
Bad href and src do not render. Avatar is an image, not an iframe.
Composer, History read-only, and Editing lock: no insert. History still encodes the type.
Clean-room suite on port 5178. Family lists and all three Dockerfiles include the package.
{ type: 'pullquote' } without a paragraph child fails schema.check().
Out of scope
Verified badge or any verified attr
Avatar upload (URL only)
Chat composer Pull quote
Convert blockquote ↔ pullquote
Steal > import or Word Quote styles
Nested pull quotes, lists, headings, or media inside the body
oEmbed / tweet-unfurl into a pull quote
Stars, company logo, multi-author, marquee, spotlight
Verdict
Ship a sixth publishable Tiptap package,
@docs.plus/extension-pull-quote. It adds one contentful wrap node,pullquote. The toolbar label is Pull quote.This is a displayed, attributed quote. It can sit Left, Center, or Right, or wrap Left or Right, the same five placements as hypermultimedia.
It does not replace StarterKit
blockquote. That node stays Quote. Indent, Markdown>, and Word Quote / Intense Quote keep using it.Visual inspiration (look only, not tokens or markup):
Do not copy those components, their
cnhelper, or their tokens. Pad skins use house tokens. The published package CSS stays host-agnostic (light-dark()literals, class prefixpq-).Why this name
Package
@docs.plus/extension-pull-quote. Directoryextensions/extension-pull-quote. Node typepullquote. Commands stay camelCase (setPullQuote).A later rename needs a schema migration. Pick this name on the first insert.
What we have today
Five publishable packages, all
2.0.0:extension-hyperlinkextension-hypermultimediaextension-indentblockquoteextension-inline-codeextension-placeholderThe pad also loads webapp-only extensions (TitleDocument, HeadingScale, HeadingFold, HeadingFilter, HeadingActions, ParagraphStyle, Highlight, MarkdownPaste, MediaUploadPlaceholder). The chat composer loads hyperlink, indent, and inline-code only. It does not load hypermultimedia.
blockquoteis already on both editors. The pad paints it as a left bar. It has no author, source, avatar, or placement attrs.Media captions are a
captionattr plus afigcaptionshell. They are not a quote node.There is no attributed-quote, testimonial, pull-quote, or
citetype in the schema or inCONTEXT.md.Tiptap contract
Reviewed against Tiptap 3 (
@tiptap/core3.31.3) and officialextension-blockquoteat ueberdosis/tiptap8623fbe. This node is a wrap, like Blockquote. It is not a media leaf and not a Details fence.name'pullquote'blockquote.group'block'heading block*.content'paragraph+'block+. This forbids list, heading, media, and nested pull quotes.definingtrueisolatingfalseliftTargetreturns null on an isolating parent.unsetPullQuotewould fail.draggablefalseNodeView.onDragStart.selectabletrue)atomfalseHTMLAttributes{}inaddOptionsOfficial Figure (
demos/…/Figure/Vue/figure.ts) is the inverse hole: content infigcaption, image from attrs. Do not copycontentElement: 'figcaption'. Pull quote puts content inblockquoteand paintsfigcaptionfrom attrs.Official Details is
definingandisolatingbecause it has two child types and custom Enter / Backspace. Pull quote has one hole. Cite is attrs. Copy Blockquote, not Details, for schema flags.Commands
Augment
@tiptap/corewith keypullQuote(camelCase), same split as officialblockQuote/'blockquote'. House surface file: typedPullQuotePublicCommandsplusdeclare module, like hyperlink.setPullQuote(attrs?)commands.wrapIn(this.name, attrs)unsetPullQuotecommands.lift(this.name)updatePullQuotecommands.updateAttributes('pullquote', attrs)setPullQuotePlacementdisplay,float,clear,margin.setPullQuoteVarianteditorialorcard.No
togglePullQuote. Official Blockquote hastoggleWrap. Official Details is set / unset only. Do not fighttoggleBlockquote.Do not call
insertContentfor set. That helper replaces the selection. House media uses it because those nodes are leaves.wrapInalready wraps an empty paragraph and a paragraph range.enableContentCheckis on. A node with no children fails.check().wrapInkeeps the selected paragraphs. A fallback that builds JSON must include{ type: 'paragraph' }. Preferthis.type.createAndFill()if a later path needs a vacant node. Do not insert{ type: 'pullquote' }alone.A selected heading cannot become a
paragraph+child.setPullQuotemust not replace the title heading.editor.can().setPullQuote()disables the toolbar when wrap cannot apply.Keyboard and input rules
Copy official Blockquote Backspace only (
handleBackspace.ts). Bind it to typepullquote.Copy nothing else from Blockquote:
wrappingInputRulefor>. That stays Quote.Mod-Shift-b. That staystoggleBlockquote.Mod-Shift-9.liftEmptyBlock.HTML
Discriminator:
figure[data-pullquote]. Innerblockquoteis the content hole.figcaptionis painted chrome.renderHTMLhas one0, and only insideblockquote. Never a second hole infigcaption.Machine attrs emit
data-*on the figure:data-author,data-role,data-href,data-src,data-variant, plus the four placement attrs. Each attr declaresparseHTML/renderHTML. Do not rely on Tiptap’s defaultgetAttribute('src')on the figure.Cite fields never use
innerHTML. Avatar renders as<img>only.Default extension priority (
100) is enough. The tag is unique. X usespriority: 101because it fights genericblockquote. Do not add a genericfigurerule. Do not add ablockquoterule.Family harden in the same change: image
getAttrsshould take:scope > img, not any descendantimg. A card avatar must not become animagenode.Node view
Vanilla
addNodeView. Required. Not optional polish. NoReactNodeViewRendererin the published package.dom= the figure.contentDOM= the innerblockquote.dom, sibling ofcontentDOM(mediaopenMediaToolbarpattern). It must not appear ingetHTML().figcaptioniscontenteditable="false". No second caret island. Do not reusecreateCaptionElement.update()returnstruefor the same type and patches cite, variant, and placement in place.ignoreMutation: official contentful default.selectionand mutations insidecontentDOMreturnfalse. Toolbar, figcaption, wrapper, anddom === targetreturntrue.ignoreNodeViewSubtreeMutation. That helper is for leaf media. It would swallow typing.stopEvent: stop toolbar and figcaption only. Never stop the hole.aria-live/[role="status"]/outputinside.ProseMirror.dom, not tocontentDOM.Markdown
Hooks live on the node, same as hyperlink and image. They stay inert unless the host loads
@tiptap/markdown.First ship:
markdownTokenName.parseMarkdown.markdownTokenizer.renderMarkdownonly, keyed by node namepullquote.Copy official
Blockquote.renderMarkdownfor the body. Prefix each child line with>. Then append a cite line without>. Build[Name](href)in this handler from attrs. Blank an unsafe href.Do not set
markdownTokenName: 'blockquote'. That steals every>from StarterKit.Do not spread
createBlockMarkdownSpecon first ship. That helper emits:::pullquoteand replaces the>export. Ship that grammar later if product wants a lossless hand-authored form.>import staysblockquote.Do not use
helpers.wrapInBlock('> ', …). Official Blockquote walks children itself.Product Markdown export uses
getMarkdownManager()over the migration set. A silent no-toDOMstub writes''. Encode set imports the real@docs.plus/extension-pull-quotenode, same as HyperMultimediaKit. Do not addtoDOMon a storage stub.What to build
One defining wrap node. The quote body is real
paragraph+content, so bold, italic, hyperlink, and inline code work.Cite fields are attrs on the wrapper, not a second content hole:
author— display namerole— short taglinehref— source or profile linksrc— optional avatar URLvariant—editorial(default) orcarddisplay,float,clear,margin(same four names as media)Do not join
HyperMultimediaKit. A pull quote is text with a cite, not a player. It must not gain grippers, Replace URL, Download, or the loading shell.Do not import
@docs.plus/extension-hypermultimediafor placement. Copy the small helper. A first-ship extract into a shared package is YAGNI. Do not import that package for caption orignoreMutationeither.Placement
Same labels as the media Align submenu:
inlinecentermargin: autorightfloat-leftfloat-rightWrap keeps the media margin presets. Default wrap gap is
0.5in. Floated boxes must stay inside the editor column (width plus wrap margins).Wrap plus a typed
contentDOMis the hard path. Media wrap works because media nodes are leaves. Schema stores all five placements from day one. Prove wrap in the browser (desktop and iOS) before the toolbar shows those two rows. If wrap fails, ship Left / Center / Right and keep the attrs.No stored pixel width. No resize gripper. Wrap width is CSS (
max-widthon the figure). Do not use officialResizableNodeView.Variants
One node. One
variantattr.editorialcardsrchides the image.The whole figure is not one link. Nested links fight contenteditable. The author name is the link when
hrefis set.No verified badge on first ship. Anyone who can edit the pad can tick a box. A check next to a name reads as platform identity. That is a lie.
Host hooks
Host-agnostic surfaces, same split as hyperlink and hypermultimedia:
role="toolbar"): placement, variant, edit attribution, delete.quoteToolbar,quoteActions,quoteToolbarIcons,popovers.editAttribution.The package ships a small prebuilt form so the playground and npm hosts work. Webapp replaces it with React.
Host may inject Comment through
quoteActions. Comment on the body as a text anchor. Do not add this node toMediaNodeType. Jump must not key on avatarsrc. Do not stamp UniqueID onpullquote.Family and pad seams
This is a new workspace member. The same change must:
scripts/publishable-extensions.ts(clean-room port 5178)..github/filters/extensions.yaml.package.jsoninto all three Dockerfiles. Frozen lockfile fails without that. Hocuspocus also needs source plus a build, because the encode set will import the node.apps/hocuspocus.server/src/lib/migration-extensions.tsbefore any client can insert. List every attr with a default. A missing stored type fails encode. It is not a flatten bug.TipTap.tsxonly. Not in the chat composer.data-testid="toolbar-pull-quote", with a distinct icon.{ textblock: 'paragraph', parent: 'pullquote' }to pad Indent only. Do not change the indent package default.Pull quote. Keepblockquote: 'Quote'.TIPTAP_NODES.PULLQUOTE_TYPE.CONTEXT.mdglossary row so prose does not rotate names.@tiptap/coreand@tiptap/pmwith the family pin (3.31.3).Do not stamp UniqueID on
pullquote. It is not a TOC target.Ship webapp from
workspace:*first. Publish on the next family release. Do not cut a lone npm version.Old tabs without the type can freeze or drop the subtree when a new tab inserts one. Deploy server schema, then pad insert. Long-lived tabs need a refresh.
Do not add
pullquotetoLEGACY_STRIP_BLOCK_PARENTSunless a real nested-heading corpus exists.paragraph+makes that list the wrong home.Leave
markdownPastePlugin.tsweighting>asblockquote. Do not add a pull-quote detector for>.URL gates
Name the link attr
hrefand the avatar attrsrc. Paste and Settings import only rewrite those two names.href: same scheme floor as hyperlink.http,https, andmailtoonly. Blank a bad value on parse, command, and render. Do notwindow.opena raw stored value.src: same write floor as media. Noblob:on write. No SVGdata:image. Render as<img>only. Do not add an iframe host for an author site.innerHTML.Do not hang these fields on
blockquote. An old schema drops unknown attrs on re-encode. A later save by an old tab would strip the cite and keep only the text.Collaborative attrs are not re-validated on sync. Blank a bad
href/srcinrenderHTMLand in the node view. UseupdateAttributes/setNodeMarkup. NeversetContentto edit cite fields.Markdown, HTML, and conversion
HTML copy must round-trip through
figure[data-pullquote]. Clean-room Cypress coversgetHTML()→setContent(), same rule as the media kit. Assert the toolbar is absent fromgetHTML().Markdown is lossy on purpose. Export the body as
>lines. A present author becomes a cite line outside the prefix (— Name, Role, optional link). Dropvariant, placement, andsrc.Import of
>staysblockquote. Do not wait for #244 option blocks. Most media attrs already die on product Markdown export. A pull quote that needs those attrs to survive GFM is the wrong design.DOCX / ODT: first ship may fall through to children. Word Quote styles stay
blockquote. Do not fetch a third-party avatar during export.toPortableJsondoes not degrade this node (it is not an embed). Conversion must see realparseHTML/renderHTML.Composer schema will drop a pasted pull quote. Accept that.
Related
Suggested slices
defining,paragraph+,group: 'block'), URL gate, CSS, playground, vanilla NodeView,contentElement. Encode set imports the real node. No pad insert yet.setPullQuote/unsetPullQuote/update*), Backspace helper, in-node toolbar hooks, HTML round-trip Cypress,javascript:href/srcrefused,>does not becomepullquote.renderMarkdownemits>plus a cite line.:scope > img.TipTap.tsx, toolbars, placeholder, indent rule, React attribution form and mobile sheet.Slice 1 must land the encode-set registration before slice 4 turns on insert.
Acceptance criteria
setPullQuote. Blockquote still togglesblockquote.Pull quote. Empty blockquote hint still readsQuote.href, andsrc. Figcaption updates. No second caret island.getHTML()→setContent()keeps the node, body, cite attrs, variant, and placement.getHTML()has no toolbar.figure[data-hm-figure]stays image.blockquote.twitter-tweetstays X. Bare<blockquote>and>stay Quote. Avatarimgdoes not becomeimage.>plus a cite line outside the prefix. Markdown import of>is stillblockquote.hrefandsrcdo not render. Avatar is an image, not an iframe.{ type: 'pullquote' }without a paragraph child failsschema.check().Out of scope
verifiedattrblockquote↔pullquote>import or Word Quote stylesCommentAnchorV1kind:::pullquote/createBlockMarkdownSpecon first shipdraggable: trueand a copiedonDragStarttogglePullQuoteor any Quote shortcutReactNodeViewRendererin the published packageOpen questions