Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
d718b7f
fix(export): add presentation option to renderStaticHtml and exportDocx
natamox Oct 10, 2026
d295f31
test(export): satisfy pnpm check gates for the presentation commit
natamox Oct 10, 2026
332c1a1
fix(static): compile presentation detached and draw unowned elements …
natamox Oct 10, 2026
0b6281a
fix(export): report failed exports and skip Copilot ghost text
natamox Oct 10, 2026
e087f26
feat(docx): export equations as Word math and tasks as checkbox items
natamox Oct 10, 2026
6e086e3
fix(registry): let BaseIndentKit and BaseListKit target images
natamox Oct 10, 2026
e730c68
fix(docx): keep a list item that holds only a display equation
natamox Oct 10, 2026
ca545c7
fix(static): accept undefined editor or document in renderStaticHtml …
natamox Oct 10, 2026
ce7657b
docs(plans): record the pull request panel for static export
natamox Oct 10, 2026
d07cb12
docs(docx): describe the equation fallback as the converter's coverage
natamox Oct 10, 2026
af50309
docs(plans): fold the html subject and record the trail review and re…
natamox Oct 10, 2026
9762612
docs(plans): cite the Word plan's tracked proof tests as review inputs
zbeyens Oct 10, 2026
b26356f
refactor(docx): render body and comment bodies through renderDocxHtml
zbeyens Oct 10, 2026
bea6eae
refactor(docx): drop equation kinds from the import Marker type
zbeyens Oct 10, 2026
b17a467
refactor(docx): write invisible math characters as escapes
zbeyens Oct 10, 2026
6af385d
docs(plans): track the Word converter findings of the PR review
zbeyens Oct 10, 2026
440f959
docs(plans): record the PR 5157 babysit
zbeyens Oct 10, 2026
e5892e8
docs(plans): settle the PR 5157 decision-trail review
zbeyens Oct 10, 2026
f46a308
docs(plans): point the checker default at the first session
zbeyens Oct 10, 2026
f8a6d34
Merge remote-tracking branch 'origin/next' into HEAD
zbeyens Oct 11, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .changeset/platejs-static-presentation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
'platejs': patch
---

- Add `presentation` to `renderStaticHtml` and `exportDocx`. Pass the app's static kit, such as `BaseEditorKit`, when exporting a live editor, so each node draws with the static plugin of the same name instead of its editing component. A plugin with no static drawing renders without its component or wrapper and reports a `missing-static-presentation` diagnostic, which `exportDocx` counts as lost content. The kit compiles without activating its plugins.
- Throw a `TypeError` when `renderStaticHtml` `props` set `editor` or `document` to a value.
- Take DOCX list levels from the indent of the block a list wraps when the list has no margin of its own.
- Fix DOCX export failing when an element that the stylesheet styles has a quoted value in its inline style, such as a callout's emoji font list.
- Mark the drawings of `CopilotPlugin` edit-only, so an export with `presentation` leaves out its ghost text instead of reporting a `missing-static-presentation` diagnostic.
- Export equations as Word math built from KaTeX's MathML. An equation that uses notation the converter does not cover, such as a matrix, exports its TeX as text inside a Word equation.
- Import Word math in the body as `equation` and `inlineEquation` nodes, with TeX rebuilt from the equation's structure, and in comment bodies as TeX text. Import flattens a tracked revision inside an equation and reports an `unsupported-content` diagnostic.
- Export a task list item, an `li` with `data-checked`, with a ☑ or ☐ list marker at its list level.
- Leave colors, fonts and sizes of the DOCX drawings to `stylesheet`. A callout draws as a borderless layout table shaded with its own color, and the code block and placeholder drawings carry no inline styles.
- Rebuild an unchanged retained `source` when the caller passes `stylesheet` or `fontFamily`, instead of returning its original file.
- Give concurrent `exportDocx` calls their own list numbering.
2 changes: 1 addition & 1 deletion apps/www/public/r/authored-changes-docs.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion apps/www/public/r/block-list-static.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"files": [
{
"path": "src/registry/components/editor/block-list-static.tsx",
"content": "import { CheckIcon } from 'lucide-react';\nimport {\n BaseListPlugin,\n isOrderedList,\n ListType,\n type RenderStaticNodeWrapper,\n} from 'platejs';\nimport * as React from 'react';\n\nimport { cn } from '@/lib/utils';\n\ntype ListWrapper = RenderStaticNodeWrapper<typeof BaseListPlugin>;\n\nexport const BlockListStatic: ListWrapper = (props) => {\n const { listStyle, listType } = props.element;\n\n if (!listType || listType === ListType.Bulleted) return undefined;\n\n return function BlockListStaticWrapper(innerProps) {\n return (\n <List\n {...innerProps}\n listStart={innerProps.editor\n .plugin(BaseListPlugin)\n .read.ordinal(innerProps.path)}\n listStyle={listStyle}\n listType={listType}\n />\n );\n };\n};\n\nfunction List(\n props: Parameters<NonNullable<ReturnType<ListWrapper>>>[0] & {\n listStart?: number;\n listStyle?: string;\n listType: ListType;\n }\n) {\n const { listStart, listStyle, listType } = props;\n const isTask = listType === ListType.Task;\n const InnerList = isOrderedList(props.element) ? 'ol' : 'ul';\n const markerStyle =\n listStyle ?? (listType === ListType.Numbered ? 'decimal' : 'none');\n\n return (\n <InnerList\n className=\"relative m-0 p-0\"\n data-list-id={props.element.listId ?? ''}\n data-list-levels={\n props.element.listLevels\n ? JSON.stringify(props.element.listLevels)\n : undefined\n }\n style={{ listStyleType: markerStyle }}\n start={listType === ListType.Numbered ? listStart : undefined}\n >\n {isTask && <TodoMarkerStatic {...props} />}\n <li\n data-checked={\n isTask ? String(props.element.checked === true) : undefined\n }\n data-indent={props.element.indent}\n data-list-id={props.element.listId ?? ''}\n data-list-levels={\n props.element.listLevels\n ? JSON.stringify(props.element.listLevels)\n : undefined\n }\n data-list-restart={props.element.listRestart}\n data-list-start={props.element.listStart}\n data-list-type={listType}\n className={\n isTask\n ? cn(\n 'list-none',\n props.element.checked === true &&\n 'text-muted-foreground line-through'\n )\n : undefined\n }\n >\n {props.children}\n </li>\n </InnerList>\n );\n}\n\nfunction TodoMarkerStatic(\n props: Parameters<NonNullable<ReturnType<ListWrapper>>>[0] & {\n listStart?: number;\n listStyle?: string;\n listType: ListType;\n }\n) {\n const checked = props.element.checked === true;\n\n return (\n <div contentEditable={false}>\n <button\n className={cn(\n 'peer -left-6 pointer-events-none absolute top-1 size-4 shrink-0 rounded-sm border border-primary bg-background ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground',\n props.className\n )}\n data-state={checked ? 'checked' : 'unchecked'}\n type=\"button\"\n >\n <div className={cn('flex items-center justify-center text-current')}>\n {checked && <CheckIcon className=\"size-4\" />}\n </div>\n </button>\n </div>\n );\n}\n",
"content": "import { CheckIcon } from 'lucide-react';\nimport {\n BaseListPlugin,\n isOrderedList,\n ListType,\n type RenderStaticNodeWrapper,\n} from 'platejs';\nimport * as React from 'react';\n\nimport { cn } from '@/lib/utils';\n\ntype ListWrapper = RenderStaticNodeWrapper<typeof BaseListPlugin>;\n\nexport const BlockListStatic: ListWrapper = (props) => {\n const { listStyle, listType } = props.element;\n\n if (!listType || listType === ListType.Bulleted) return undefined;\n\n return function BlockListStaticWrapper(innerProps) {\n return (\n <List\n {...innerProps}\n listStart={innerProps.editor\n .plugin(BaseListPlugin)\n .read.ordinal(innerProps.path)}\n listStyle={listStyle}\n listType={listType}\n />\n );\n };\n};\n\nfunction List(\n props: Parameters<NonNullable<ReturnType<ListWrapper>>>[0] & {\n listStart?: number;\n listStyle?: string;\n listType: ListType;\n }\n) {\n const { listStart, listStyle, listType } = props;\n const isTask = listType === ListType.Task;\n const InnerList = isOrderedList(props.element) ? 'ol' : 'ul';\n const markerStyle =\n listStyle ?? (listType === ListType.Numbered ? 'decimal' : 'none');\n\n return (\n <InnerList\n className=\"relative m-0 p-0\"\n data-list-id={props.element.listId ?? ''}\n data-list-levels={\n props.element.listLevels\n ? JSON.stringify(props.element.listLevels)\n : undefined\n }\n style={{ listStyleType: markerStyle }}\n start={listType === ListType.Numbered ? listStart : undefined}\n >\n <li\n data-checked={\n isTask ? String(props.element.checked === true) : undefined\n }\n data-indent={props.element.indent}\n data-list-id={props.element.listId ?? ''}\n data-list-levels={\n props.element.listLevels\n ? JSON.stringify(props.element.listLevels)\n : undefined\n }\n data-list-restart={props.element.listRestart}\n data-list-start={props.element.listStart}\n data-list-type={listType}\n className={\n isTask\n ? cn(\n 'list-none',\n props.element.checked === true &&\n 'text-muted-foreground line-through'\n )\n : undefined\n }\n >\n {isTask && <TodoMarkerStatic {...props} />}\n {props.children}\n </li>\n </InnerList>\n );\n}\n\nfunction TodoMarkerStatic(\n props: Parameters<NonNullable<ReturnType<ListWrapper>>>[0] & {\n listStart?: number;\n listStyle?: string;\n listType: ListType;\n }\n) {\n const checked = props.element.checked === true;\n\n return (\n <button\n className={cn(\n 'peer -left-6 pointer-events-none absolute top-1 size-4 shrink-0 rounded-sm border border-primary bg-background ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground',\n props.className\n )}\n data-state={checked ? 'checked' : 'unchecked'}\n type=\"button\"\n >\n <div className={cn('flex items-center justify-center text-current')}>\n {checked && <CheckIcon className=\"size-4\" />}\n </div>\n </button>\n );\n}\n",
"type": "registry:component",
"target": "@components/editor/block-list-static.tsx"
}
Expand Down
2 changes: 1 addition & 1 deletion apps/www/public/r/docx-docs.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion apps/www/public/r/docx-export.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"files": [
{
"path": "src/registry/components/editor/docx-export.tsx",
"content": "export const DOCX_EXPORT_STYLES = `\nbody {\n font-family: 'Calibri', 'Arial', sans-serif;\n font-size: 11pt;\n line-height: 1.5;\n color: #000;\n margin: 0;\n padding: 20px;\n}\nh1 { font-size: 24pt; font-weight: bold; margin: 0 0 12pt 0; }\nh2 { font-size: 18pt; font-weight: bold; margin: 0 0 10pt 0; }\nh3 { font-size: 14pt; font-weight: bold; margin: 0 0 8pt 0; }\nh4 { font-size: 12pt; font-weight: bold; margin: 0 0 6pt 0; }\nh5 { font-size: 11pt; font-weight: bold; margin: 0 0 6pt 0; }\nh6 { font-size: 10pt; font-weight: bold; margin: 0 0 6pt 0; }\np { margin: 0 0 8pt 0; }\nul, ol { margin: 0 0 8pt 0; padding-left: 20pt; }\nli { margin: 0 0 4pt 0; }\nstrong, b { font-weight: bold; }\nem, i { font-style: italic; }\nu { text-decoration: underline; }\ns, strike, del { text-decoration: line-through; }\ncode {\n font-family: 'Courier New', Consolas, monospace;\n background-color: #f5f5f5;\n padding: 2px 4px;\n border-radius: 3px;\n}\npre {\n font-family: 'Courier New', Consolas, monospace;\n background-color: #f5f5f5;\n padding: 10px;\n margin: 0 0 8pt 0;\n white-space: pre-wrap;\n border-radius: 4px;\n}\n.hljs-addition, .hljs-name, .hljs-quote, .hljs-selector-pseudo, .hljs-selector-tag { color: #22863a; }\n.hljs-attr, .hljs-attribute, .hljs-literal, .hljs-meta, .hljs-number, .hljs-operator,\n.hljs-section, .hljs-selector-attr, .hljs-selector-class, .hljs-selector-id, .hljs-variable { color: #005cc5; }\n.hljs-built_in, .hljs-symbol { color: #e36209; }\n.hljs-bullet { color: #735c0f; }\n.hljs-comment, .hljs-formula { color: #6a737d; }\n.hljs-deletion { color: #b31d28; }\n.hljs-doctag, .hljs-keyword, .hljs-template-tag, .hljs-template-variable, .hljs-type { color: #d73a49; }\n.hljs-regexp, .hljs-string { color: #032f62; }\n.hljs-title { color: #6f42c1; }\n.hljs-emphasis { font-style: italic; }\n.hljs-section, .hljs-strong { font-weight: bold; }\nblockquote {\n border-left: 3px solid #ccc;\n margin: 0 0 8pt 0;\n padding-left: 10pt;\n color: #666;\n font-style: italic;\n}\ntable {\n border-collapse: collapse;\n width: 100%;\n margin: 0 0 8pt 0;\n}\nth, td {\n border: 1px solid #ccc;\n padding: 6pt;\n text-align: left;\n}\nth {\n background-color: #f5f5f5;\n font-weight: bold;\n}\na {\n color: #0066cc;\n text-decoration: underline;\n}\nimg {\n max-width: 100%;\n height: auto;\n}\nhr {\n border: none;\n border-top: 1px solid #ccc;\n margin: 12pt 0;\n}\nsup { vertical-align: super; font-size: 8pt; }\nsub { vertical-align: sub; font-size: 8pt; }\nmark { background-color: #ffff00; }\n`.trim();\n",
"content": "export const DOCX_EXPORT_STYLES = `\nbody {\n font-family: 'Calibri', 'Arial', sans-serif;\n font-size: 11pt;\n line-height: 1.5;\n color: #000;\n margin: 0;\n padding: 20px;\n}\nh1 { font-size: 24pt; font-weight: bold; margin: 0 0 12pt 0; }\nh2 { font-size: 18pt; font-weight: bold; margin: 0 0 10pt 0; }\nh3 { font-size: 14pt; font-weight: bold; margin: 0 0 8pt 0; }\nh4 { font-size: 12pt; font-weight: bold; margin: 0 0 6pt 0; }\nh5 { font-size: 11pt; font-weight: bold; margin: 0 0 6pt 0; }\nh6 { font-size: 10pt; font-weight: bold; margin: 0 0 6pt 0; }\np { margin: 0 0 8pt 0; }\nul, ol { margin: 0 0 8pt 0; padding-left: 20pt; }\nli { margin: 0 0 4pt 0; }\nstrong, b { font-weight: bold; }\nem, i { font-style: italic; }\nu { text-decoration: underline; }\ns, strike, del { text-decoration: line-through; }\ncode {\n font-family: 'Courier New', Consolas, monospace;\n background-color: #f5f5f5;\n padding: 2px 4px;\n border-radius: 3px;\n}\npre {\n font-family: 'Courier New', Consolas, monospace;\n background-color: #f5f5f5;\n padding: 10px;\n margin: 0 0 8pt 0;\n white-space: pre-wrap;\n border-radius: 4px;\n}\n.hljs-addition, .hljs-name, .hljs-quote, .hljs-selector-pseudo, .hljs-selector-tag { color: #22863a; }\n.hljs-attr, .hljs-attribute, .hljs-literal, .hljs-meta, .hljs-number, .hljs-operator,\n.hljs-section, .hljs-selector-attr, .hljs-selector-class, .hljs-selector-id, .hljs-variable { color: #005cc5; }\n.hljs-built_in, .hljs-symbol { color: #e36209; }\n.hljs-bullet { color: #735c0f; }\n.hljs-comment, .hljs-formula { color: #6a737d; }\n.hljs-deletion { color: #b31d28; }\n.hljs-doctag, .hljs-keyword, .hljs-template-tag, .hljs-template-variable, .hljs-type { color: #d73a49; }\n.hljs-regexp, .hljs-string { color: #032f62; }\n.hljs-title { color: #6f42c1; }\n.hljs-emphasis { font-style: italic; }\n.hljs-section, .hljs-strong { font-weight: bold; }\nblockquote {\n border-left: 3px solid #ccc;\n margin: 0 0 8pt 0;\n padding-left: 10pt;\n color: #666;\n font-style: italic;\n}\ntable {\n border-collapse: collapse;\n width: 100%;\n margin: 0 0 8pt 0;\n}\nth, td {\n border: 1px solid #ccc;\n padding: 6pt;\n text-align: left;\n}\nth {\n background-color: #f5f5f5;\n font-weight: bold;\n}\na {\n color: #0066cc;\n text-decoration: underline;\n}\nimg {\n max-width: 100%;\n height: auto;\n}\nhr {\n border: none;\n border-top: 1px solid #ccc;\n margin: 12pt 0;\n}\nsup { vertical-align: super; font-size: 8pt; }\nsub { vertical-align: sub; font-size: 8pt; }\nmark { background-color: #ffff00; }\n.editor-callout > table > tbody > tr > td { background-color: #f4f4f5; }\n.editor-callout > table > tbody > tr > td:first-child {\n font-family: \"Apple Color Emoji\", \"Segoe UI Emoji\", NotoColorEmoji, \"Noto Color Emoji\", \"Segoe UI Symbol\", \"Android Emoji\", EmojiSymbols;\n font-size: 18px;\n}\n.editor-codeBlock > [data-docx-preserve-whitespace] {\n background-color: #f5f5f5;\n font-family: 'Courier New', Consolas, monospace;\n font-size: 10pt;\n}\n.editor-equation [data-empty],\n.editor-inlineEquation [data-empty] { color: #888; }\n.editor-toc [data-empty] { color: #666; font-size: 10pt; }\n`.trim();\n",
"type": "registry:component",
"target": "@components/editor/docx-export.tsx"
}
Expand Down
Loading
Loading