@prosekit/extensions 0.14.2 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (151) hide show
  1. package/dist/commit/style.css +1 -3
  2. package/dist/{drop-indicator-DJq8pF92.js → drop-indicator.js} +2 -4
  3. package/dist/drop-indicator.js.map +1 -0
  4. package/dist/{file-upload-I9m1EJAM.js → file.js} +2 -6
  5. package/dist/file.js.map +1 -0
  6. package/dist/gap-cursor/style.css +5 -8
  7. package/dist/{file-upload-dr3IXUty.d.ts → index.d.ts} +1 -1
  8. package/dist/index.d.ts.map +1 -0
  9. package/dist/list/style.css +79 -110
  10. package/dist/loro/style.css +18 -21
  11. package/dist/{mark-rule-Bqdm49Eq.js → mark-rule.js} +2 -5
  12. package/dist/mark-rule.js.map +1 -0
  13. package/dist/page/style.css +43 -0
  14. package/dist/{mark-paste-rule--F1QPUcU.js → paste-rule.js} +2 -6
  15. package/dist/paste-rule.js.map +1 -0
  16. package/dist/placeholder/style.css +4 -7
  17. package/dist/prosekit-extensions-autocomplete.js +1 -5
  18. package/dist/prosekit-extensions-autocomplete.js.map +1 -1
  19. package/dist/prosekit-extensions-background-color.js +1 -4
  20. package/dist/prosekit-extensions-background-color.js.map +1 -1
  21. package/dist/prosekit-extensions-blockquote.js +1 -6
  22. package/dist/prosekit-extensions-blockquote.js.map +1 -1
  23. package/dist/prosekit-extensions-bold.js +1 -6
  24. package/dist/prosekit-extensions-bold.js.map +1 -1
  25. package/dist/prosekit-extensions-code-block.d.ts +4 -2
  26. package/dist/prosekit-extensions-code-block.d.ts.map +1 -1
  27. package/dist/prosekit-extensions-code-block.js +1 -10
  28. package/dist/prosekit-extensions-code-block.js.map +1 -1
  29. package/dist/prosekit-extensions-code.js +1 -6
  30. package/dist/prosekit-extensions-code.js.map +1 -1
  31. package/dist/prosekit-extensions-commit.js +1 -2
  32. package/dist/prosekit-extensions-commit.js.map +1 -1
  33. package/dist/prosekit-extensions-doc.js +1 -2
  34. package/dist/prosekit-extensions-doc.js.map +1 -1
  35. package/dist/prosekit-extensions-drop-cursor.js +1 -2
  36. package/dist/prosekit-extensions-drop-cursor.js.map +1 -1
  37. package/dist/prosekit-extensions-drop-indicator.js +2 -3
  38. package/dist/prosekit-extensions-enter-rule.js +1 -2
  39. package/dist/prosekit-extensions-enter-rule.js.map +1 -1
  40. package/dist/prosekit-extensions-file.d.ts +2 -2
  41. package/dist/prosekit-extensions-file.js +2 -3
  42. package/dist/prosekit-extensions-gap-cursor.js +1 -2
  43. package/dist/prosekit-extensions-gap-cursor.js.map +1 -1
  44. package/dist/prosekit-extensions-hard-break.js +1 -5
  45. package/dist/prosekit-extensions-hard-break.js.map +1 -1
  46. package/dist/prosekit-extensions-heading.js +1 -6
  47. package/dist/prosekit-extensions-heading.js.map +1 -1
  48. package/dist/prosekit-extensions-horizontal-rule.d.ts.map +1 -1
  49. package/dist/prosekit-extensions-horizontal-rule.js +3 -6
  50. package/dist/prosekit-extensions-horizontal-rule.js.map +1 -1
  51. package/dist/prosekit-extensions-image.d.ts +1 -2
  52. package/dist/prosekit-extensions-image.d.ts.map +1 -1
  53. package/dist/prosekit-extensions-image.js +2 -8
  54. package/dist/prosekit-extensions-image.js.map +1 -1
  55. package/dist/prosekit-extensions-input-rule.js +1 -2
  56. package/dist/prosekit-extensions-input-rule.js.map +1 -1
  57. package/dist/prosekit-extensions-italic.js +1 -6
  58. package/dist/prosekit-extensions-italic.js.map +1 -1
  59. package/dist/prosekit-extensions-link.js +3 -6
  60. package/dist/prosekit-extensions-link.js.map +1 -1
  61. package/dist/prosekit-extensions-list.js +2 -10
  62. package/dist/prosekit-extensions-list.js.map +1 -1
  63. package/dist/prosekit-extensions-loro.d.ts.map +1 -1
  64. package/dist/prosekit-extensions-loro.js +3 -9
  65. package/dist/prosekit-extensions-loro.js.map +1 -1
  66. package/dist/prosekit-extensions-mark-rule.js +2 -3
  67. package/dist/prosekit-extensions-math.js +1 -5
  68. package/dist/prosekit-extensions-math.js.map +1 -1
  69. package/dist/prosekit-extensions-mention.js +1 -2
  70. package/dist/prosekit-extensions-mention.js.map +1 -1
  71. package/dist/prosekit-extensions-mod-click-prevention.js +1 -2
  72. package/dist/prosekit-extensions-mod-click-prevention.js.map +1 -1
  73. package/dist/prosekit-extensions-page.d.ts +114 -0
  74. package/dist/prosekit-extensions-page.d.ts.map +1 -0
  75. package/dist/prosekit-extensions-page.js +324 -0
  76. package/dist/prosekit-extensions-page.js.map +1 -0
  77. package/dist/prosekit-extensions-paragraph.d.ts.map +1 -1
  78. package/dist/prosekit-extensions-paragraph.js +3 -7
  79. package/dist/prosekit-extensions-paragraph.js.map +1 -1
  80. package/dist/prosekit-extensions-paste-rule.js +2 -3
  81. package/dist/prosekit-extensions-placeholder.js +2 -3
  82. package/dist/prosekit-extensions-placeholder.js.map +1 -1
  83. package/dist/prosekit-extensions-readonly.js +1 -2
  84. package/dist/prosekit-extensions-readonly.js.map +1 -1
  85. package/dist/prosekit-extensions-search.js +1 -2
  86. package/dist/prosekit-extensions-search.js.map +1 -1
  87. package/dist/prosekit-extensions-strike.js +1 -2
  88. package/dist/prosekit-extensions-strike.js.map +1 -1
  89. package/dist/prosekit-extensions-table.js +2 -3
  90. package/dist/prosekit-extensions-text-align.js +1 -2
  91. package/dist/prosekit-extensions-text-align.js.map +1 -1
  92. package/dist/prosekit-extensions-text-color.js +1 -4
  93. package/dist/prosekit-extensions-text-color.js.map +1 -1
  94. package/dist/prosekit-extensions-text.js +1 -2
  95. package/dist/prosekit-extensions-text.js.map +1 -1
  96. package/dist/prosekit-extensions-underline.js +1 -2
  97. package/dist/prosekit-extensions-underline.js.map +1 -1
  98. package/dist/prosekit-extensions-virtual-selection.js +1 -2
  99. package/dist/prosekit-extensions-virtual-selection.js.map +1 -1
  100. package/dist/prosekit-extensions-yjs.d.ts.map +1 -1
  101. package/dist/prosekit-extensions-yjs.js +3 -9
  102. package/dist/prosekit-extensions-yjs.js.map +1 -1
  103. package/dist/prosekit-extensions.js +1 -1
  104. package/dist/search/style.css +4 -8
  105. package/dist/shiki-highlighter-chunk.js +1 -2
  106. package/dist/shiki-highlighter-chunk.js.map +1 -1
  107. package/dist/table/style.css +16 -16
  108. package/dist/{table-B81i9oH9.js → table.js} +3 -15
  109. package/dist/table.js.map +1 -0
  110. package/dist/virtual-selection/style.css +1 -4
  111. package/dist/yjs/style.css +14 -20
  112. package/package.json +27 -14
  113. package/src/horizontal-rule/horizontal-rule-commands.ts +2 -1
  114. package/src/loro/loro.ts +3 -2
  115. package/src/page/index.ts +5 -0
  116. package/src/page/page-break-commands.spec.ts +61 -0
  117. package/src/page/page-break-commands.ts +41 -0
  118. package/src/page/page-break-keymap.ts +17 -0
  119. package/src/page/page-break-spec.ts +33 -0
  120. package/src/page/page-break.ts +23 -0
  121. package/src/page/page-element.ts +246 -0
  122. package/src/page/page-rendering.ts +164 -0
  123. package/src/page/style.css +43 -0
  124. package/src/paragraph/paragraph.ts +3 -2
  125. package/src/yjs/yjs.ts +3 -2
  126. package/dist/commit/style.css.map +0 -1
  127. package/dist/commit/style.js +0 -1
  128. package/dist/drop-indicator-DJq8pF92.js.map +0 -1
  129. package/dist/file-upload-I9m1EJAM.js.map +0 -1
  130. package/dist/file-upload-dr3IXUty.d.ts.map +0 -1
  131. package/dist/gap-cursor/style.css.map +0 -1
  132. package/dist/gap-cursor/style.js +0 -1
  133. package/dist/list/style.css.map +0 -1
  134. package/dist/list/style.js +0 -1
  135. package/dist/loro/style.css.map +0 -1
  136. package/dist/loro/style.js +0 -1
  137. package/dist/mark-paste-rule--F1QPUcU.js.map +0 -1
  138. package/dist/mark-rule-Bqdm49Eq.js.map +0 -1
  139. package/dist/placeholder/style.css.map +0 -1
  140. package/dist/placeholder/style.js +0 -1
  141. package/dist/search/style.css.map +0 -1
  142. package/dist/search/style.js +0 -1
  143. package/dist/shiki-highlighter-chunk.d.ts +0 -19
  144. package/dist/shiki-highlighter-chunk.d.ts.map +0 -1
  145. package/dist/table/style.css.map +0 -1
  146. package/dist/table/style.js +0 -1
  147. package/dist/table-B81i9oH9.js.map +0 -1
  148. package/dist/virtual-selection/style.css.map +0 -1
  149. package/dist/virtual-selection/style.js +0 -1
  150. package/dist/yjs/style.css.map +0 -1
  151. package/dist/yjs/style.js +0 -1
@@ -0,0 +1 @@
1
+ {"version":3,"file":"table.js","names":[],"sources":["../src/table/table-commands/exit-table.ts","../src/table/table-commands/insert-table.ts","../src/table/table-utils.ts","../src/table/table-commands/select-table-cell.ts","../src/table/table-commands/select-table-column.ts","../src/table/table-commands/select-table-row.ts","../src/table/table-commands/select-table.ts","../src/table/table-commands.ts","../src/table/table-drop-indicator.ts","../src/table/table-plugins.ts","../src/table/table-spec.ts","../src/table/table.ts"],"sourcesContent":["import { defaultBlockAt } from '@prosekit/core'\nimport { TextSelection } from '@prosekit/pm/state'\nimport type { Command } from '@prosekit/pm/state'\nimport type { TableRole } from 'prosemirror-tables'\n\n/**\n * When the selection is in a table node, create a default block after the table\n * table, and move the cursor there.\n *\n * @public\n */\nexport const exitTable: Command = (state, dispatch) => {\n const { $head, $anchor } = state.selection\n\n if (!$head.sameParent($anchor)) {\n return false\n }\n\n let tableStart = -1\n let tableDepth = -1\n for (let depth = $head.depth; depth >= 0; depth--) {\n const node = $head.node(depth)\n if ((node.type.spec.tableRole as TableRole) === 'table') {\n tableStart = $head.before(depth)\n tableDepth = depth\n }\n }\n\n if (tableStart < 0 || tableDepth <= 0) {\n return false\n }\n\n const above = $head.node(tableDepth - 1)\n const after = $head.indexAfter(tableDepth - 1)\n const type = defaultBlockAt(above.contentMatchAt(after))\n const node = type?.createAndFill()\n\n if (!type || !node || !above.canReplaceWith(after, after, type)) {\n return false\n }\n\n if (dispatch) {\n const pos = $head.after(tableDepth)\n const tr = state.tr.replaceWith(pos, pos, node)\n tr.setSelection(TextSelection.near(tr.doc.resolve(pos), 1))\n dispatch(tr.scrollIntoView())\n }\n return true\n}\n","import { getNodeType, insertNode } from '@prosekit/core'\nimport type { Schema } from '@prosekit/pm/model'\nimport type { Command } from '@prosekit/pm/state'\n\nfunction createEmptyTable(\n schema: Schema,\n row: number,\n col: number,\n header: boolean,\n) {\n const tableType = getNodeType(schema, 'table')\n const tableRowType = getNodeType(schema, 'tableRow')\n const tableCellType = getNodeType(schema, 'tableCell')\n const tableHeaderCellType = getNodeType(schema, 'tableHeaderCell')\n\n if (header) {\n const headerCell = tableHeaderCellType.createAndFill()!\n const headerCells = repeat(headerCell, col)\n const headerRow = tableRowType.createAndFill(null, headerCells)!\n\n const bodyCell = tableCellType.createAndFill()!\n const bodyCells = repeat(bodyCell, col)\n const bodyRow = tableRowType.createAndFill(null, bodyCells)!\n const bodyRows = repeat(bodyRow, row - 1)\n\n return tableType.createAndFill(null, [headerRow, ...bodyRows])!\n } else {\n const bodyCell = tableCellType.createAndFill()!\n const bodyCells = repeat(bodyCell, col)\n const bodyRow = tableRowType.createAndFill(null, bodyCells)!\n const bodyRows = repeat(bodyRow, row)\n\n return tableType.createAndFill(null, bodyRows)!\n }\n}\n\nfunction repeat<T>(node: T, length: number): T[] {\n return Array<T>(length).fill(node)\n}\n\n/**\n * @public\n */\nexport interface InsertTableOptions {\n /**\n * The number of rows in the table.\n */\n row: number\n\n /**\n * The number of columns in the table.\n */\n col: number\n\n /**\n * Whether the table has a header row.\n *\n * @default false\n */\n header?: boolean\n}\n\n/**\n * Insert a table node with the given number of rows and columns, and optionally\n * a header row.\n *\n * @param options\n *\n * @public\n */\nexport function insertTable(options: InsertTableOptions): Command {\n return (state, dispatch, view) => {\n const { row, col, header = false } = options\n const table = createEmptyTable(state.schema, row, col, header)\n return insertNode({ node: table })(state, dispatch, view)\n }\n}\n","import { CellSelection, findCellPos, findCellRange, findTable } from 'prosemirror-tables'\n\nexport { findCellPos, findCellRange, findTable }\n\n/**\n * Checks if the given object is a `CellSelection` instance.\n *\n * @public\n */\nexport function isCellSelection(value: unknown): value is CellSelection {\n return value instanceof CellSelection\n}\n","import type { Command } from '@prosekit/pm/state'\nimport { CellSelection } from 'prosemirror-tables'\n\nimport { findCellPos } from '../table-utils.ts'\n\n/**\n * @public\n */\nexport interface SelectTableCellOptions {\n /**\n * A hit position of the table cell to select from. By default, the selection\n * anchor will be used.\n */\n pos?: number\n}\n\n/**\n * @public\n */\nexport function selectTableCell(options?: SelectTableCellOptions): Command {\n return (state, dispatch) => {\n const $cellPos = findCellPos(\n state.doc,\n options?.pos ?? state.selection.anchor,\n )\n if (!$cellPos) {\n return false\n }\n if (dispatch) {\n const selection = new CellSelection($cellPos)\n dispatch(state.tr.setSelection(selection))\n }\n return true\n }\n}\n","import type { Command } from '@prosekit/pm/state'\nimport { CellSelection } from 'prosemirror-tables'\n\nimport { findCellRange } from '../table-utils.ts'\n\n/**\n * @public\n */\nexport interface SelectTableColumnOptions {\n /**\n * A hit position of the table cell to select from. By default, the selection\n * anchor will be used.\n */\n anchor?: number\n\n /**\n * A hit position of the table cell to select to. By default, the selection\n * head will be used.\n */\n head?: number\n}\n\n/**\n * @public\n */\nexport function selectTableColumn(options?: SelectTableColumnOptions): Command {\n return (state, dispatch) => {\n const range = findCellRange(state.selection, options?.anchor, options?.head)\n if (!range) {\n return false\n }\n if (dispatch) {\n const [$anchorCell, $headCell] = range\n const selection = CellSelection.colSelection($anchorCell, $headCell)\n dispatch(state.tr.setSelection(selection))\n }\n return true\n }\n}\n","import type { Command } from '@prosekit/pm/state'\nimport { CellSelection } from 'prosemirror-tables'\n\nimport { findCellRange } from '../table-utils.ts'\n\n/**\n * @public\n */\nexport interface SelectTableRowOptions {\n /**\n * A hit position of the table cell to select from. By default, the selection\n * anchor will be used.\n */\n anchor?: number\n\n /**\n * A hit position of the table cell to select to. By default, the selection\n * head will be used.\n */\n head?: number\n}\n\n/**\n * @public\n */\nexport function selectTableRow(options?: SelectTableRowOptions): Command {\n return (state, dispatch) => {\n const range = findCellRange(state.selection, options?.anchor, options?.head)\n if (!range) {\n return false\n }\n if (dispatch) {\n const [$anchorCell, $headCell] = range\n const selection = CellSelection.rowSelection($anchorCell, $headCell)\n dispatch(state.tr.setSelection(selection))\n }\n return true\n }\n}\n","import type { Command } from '@prosekit/pm/state'\nimport { CellSelection, TableMap } from 'prosemirror-tables'\n\nimport { findTable } from '../table-utils.ts'\n\n/**\n * @public\n */\nexport interface SelectTableOptions {\n /**\n * A hit position of the table to select from. By default, the selection\n * anchor will be used.\n */\n pos?: number\n}\n\n/**\n * @public\n */\nexport function selectTable(options?: SelectTableOptions): Command {\n return (state, dispatch) => {\n const $pos = options?.pos\n ? state.doc.resolve(options.pos)\n : state.selection.$anchor\n const table = findTable($pos)\n if (!table) {\n return false\n }\n const map = TableMap.get(table.node)\n if (map.map.length === 0) {\n return false\n }\n if (dispatch) {\n let tr = state.tr\n const firstCellPosInTable = map.map[0]\n const lastCellPosInTable = map.map[map.map.length - 1]\n const firstCellPos = table.pos + firstCellPosInTable + 1\n const lastCellPos = table.pos + lastCellPosInTable + 1\n const $firstCellPos = tr.doc.resolve(firstCellPos)\n const $lastCellPos = tr.doc.resolve(lastCellPos)\n const selection = new CellSelection($firstCellPos, $lastCellPos)\n tr = tr.setSelection(selection)\n dispatch?.(tr)\n }\n return true\n }\n}\n","import { defineCommands, type Extension } from '@prosekit/core'\nimport {\n addColumnAfter,\n addColumnBefore,\n addRowAfter,\n addRowBefore,\n deleteColumn,\n deleteRow,\n deleteTable,\n mergeCells,\n splitCell,\n} from 'prosemirror-tables'\n\nimport { deleteCellSelection } from './table-commands/delete-cell-selection.ts'\nimport { exitTable } from './table-commands/exit-table.ts'\nimport { insertTable, type InsertTableOptions } from './table-commands/insert-table.ts'\nimport { moveTableColumn, type MoveTableColumnOptions } from './table-commands/move-table-column.ts'\nimport { moveTableRow, type MoveTableRowOptions } from './table-commands/move-table-row.ts'\nimport { selectTableCell, type SelectTableCellOptions } from './table-commands/select-table-cell.ts'\nimport { selectTableColumn, type SelectTableColumnOptions } from './table-commands/select-table-column.ts'\nimport { selectTableRow, type SelectTableRowOptions } from './table-commands/select-table-row.ts'\nimport { selectTable, type SelectTableOptions } from './table-commands/select-table.ts'\n\n/**\n * @internal\n */\nexport type TableCommandsExtension = Extension<{\n Commands: {\n insertTable: [options: InsertTableOptions]\n exitTable: []\n\n selectTable: [options?: SelectTableOptions]\n selectTableCell: [options?: SelectTableCellOptions]\n selectTableColumn: [options?: SelectTableColumnOptions]\n selectTableRow: [options?: SelectTableRowOptions]\n\n addTableColumnBefore: []\n addTableColumnAfter: []\n addTableRowAbove: []\n addTableRowBelow: []\n\n deleteTable: []\n deleteTableColumn: []\n deleteTableRow: []\n deleteCellSelection: []\n\n mergeTableCells: []\n splitTableCell: []\n\n moveTableRow: [options: MoveTableRowOptions]\n moveTableColumn: [options: MoveTableColumnOptions]\n }\n}>\n\n/**\n * Adds commands for working with `table` nodes.\n *\n * @public\n */\nexport function defineTableCommands(): TableCommandsExtension {\n return defineCommands({\n insertTable,\n exitTable: () => exitTable,\n\n selectTable,\n selectTableCell,\n selectTableColumn,\n selectTableRow,\n\n addTableColumnBefore: () => addColumnBefore,\n addTableColumnAfter: () => addColumnAfter,\n addTableRowAbove: () => addRowBefore,\n addTableRowBelow: () => addRowAfter,\n\n deleteTable: () => deleteTable,\n deleteTableColumn: () => deleteColumn,\n deleteTableRow: () => deleteRow,\n deleteCellSelection: () => deleteCellSelection,\n\n mergeTableCells: () => mergeCells,\n splitTableCell: () => splitCell,\n\n moveTableRow,\n moveTableColumn,\n })\n}\n","import type { PlainExtension } from '@prosekit/core'\n\nimport type { DragEventHandler } from '../drop-indicator/index.ts'\nimport { defineDropIndicator } from '../drop-indicator/index.ts'\n\n/**\n * Hides the drop indicator when dragging a table column or row by using the\n * table handle.\n *\n * @internal\n */\nexport function defineTableDropIndicator(): PlainExtension {\n return defineDropIndicator({\n onDrag,\n })\n}\n\nconst matchMap = new WeakMap<DataTransfer, boolean>()\n\nconst onDrag: DragEventHandler = ({ event }): boolean => {\n const dataTransfer = event.dataTransfer\n if (!dataTransfer) return true\n\n let match: boolean\n\n if (matchMap.has(dataTransfer)) {\n match = matchMap.get(dataTransfer)!\n } else {\n // On Safari, accessing `dataTransfer.types` is more than 10x slower than in\n // Chrome. This becomes a bottleneck when `onDrag` is called frequently, so\n // we cache the result in a WeakMap.\n const types = dataTransfer.types\n match = types.includes('application/x-prosekit-table-handle-drag')\n matchMap.set(dataTransfer, match)\n }\n\n // Don't show the drop indicator when the drag event has\n // \"application/x-prosekit-table-handle-drag\" type.\n return !match\n}\n","import { definePlugin, type PlainExtension } from '@prosekit/core'\nimport { columnResizing, tableEditing } from 'prosemirror-tables'\n\n/**\n * @public\n */\nexport function defineTablePlugins(): PlainExtension {\n return definePlugin([tableEditing(), columnResizing()])\n}\n","import { defineNodeSpec, type Extension } from '@prosekit/core'\nimport type { AttributeSpec, Attrs } from '@prosekit/pm/model'\nimport { tableNodes } from 'prosemirror-tables'\n\nconst cellContent = 'block+'\n\n/**\n * @public\n */\nexport interface CellAttrs {\n colspan?: number\n rowspan?: number\n colwidth?: number[] | null\n}\n\nconst cellAttrs = {\n colspan: { default: 1 },\n rowspan: { default: 1 },\n colwidth: { default: null },\n} satisfies Record<string, AttributeSpec>\n\n/**\n * @internal\n */\nexport type TableSpecExtension = Extension<{\n Nodes: {\n table: Attrs\n }\n}>\n\nconst specs = tableNodes({\n tableGroup: 'block',\n cellContent,\n cellAttributes: {},\n})\n\n/**\n * @internal\n */\nexport function defineTableSpec(): TableSpecExtension {\n return defineNodeSpec({\n ...specs['table'],\n content: 'tableRow+',\n name: 'table',\n })\n}\n\n/**\n * @internal\n */\nexport type TableRowSpecExtension = Extension<{\n Nodes: {\n tableRow: Attrs\n }\n}>\n\n/**\n * @internal\n */\nexport function defineTableRowSpec(): TableRowSpecExtension {\n return defineNodeSpec({\n ...specs['table_row'],\n content: '(tableCell | tableHeaderCell)*',\n name: 'tableRow',\n })\n}\n\n/**\n * @internal\n */\nexport type TableCellSpecExtension = Extension<{\n Nodes: {\n tableCell: CellAttrs\n }\n}>\n\n/**\n * @internal\n */\nexport function defineTableCellSpec(): TableCellSpecExtension {\n return defineNodeSpec({\n ...specs['table_cell'],\n name: 'tableCell',\n attrs: cellAttrs,\n })\n}\n\n/**\n * @internal\n */\nexport type TableHeaderCellSpecExtension = Extension<{\n Nodes: {\n tableHeaderCell: CellAttrs\n }\n}>\n\nexport function defineTableHeaderCellSpec(): TableHeaderCellSpecExtension {\n return defineNodeSpec({\n ...specs['table_header'],\n name: 'tableHeaderCell',\n attrs: cellAttrs,\n })\n}\n","import { union, type Union } from '@prosekit/core'\n\nimport { defineTableCommands, type TableCommandsExtension } from './table-commands.ts'\nimport { defineTableDropIndicator } from './table-drop-indicator.ts'\nimport { defineTablePlugins } from './table-plugins.ts'\nimport {\n defineTableCellSpec,\n defineTableHeaderCellSpec,\n defineTableRowSpec,\n defineTableSpec,\n type TableCellSpecExtension,\n type TableHeaderCellSpecExtension,\n type TableRowSpecExtension,\n type TableSpecExtension,\n} from './table-spec.ts'\n\n/**\n * @internal\n */\nexport type TableExtension = Union<\n [\n TableSpecExtension,\n TableRowSpecExtension,\n TableCellSpecExtension,\n TableHeaderCellSpecExtension,\n TableCommandsExtension,\n ]\n>\n\n/**\n * @public\n */\nexport function defineTable(): TableExtension {\n return union(\n defineTableSpec(),\n defineTableRowSpec(),\n defineTableCellSpec(),\n defineTableHeaderCellSpec(),\n defineTablePlugins(),\n defineTableCommands(),\n defineTableDropIndicator(),\n )\n}\n"],"mappings":";;;;;;;;;;;AAWA,MAAa,aAAsB,OAAO,aAAa;CACrD,MAAM,EAAE,OAAO,YAAY,MAAM;AAEjC,KAAI,CAAC,MAAM,WAAW,QAAQ,CAC5B,QAAO;CAGT,IAAI,aAAa;CACjB,IAAI,aAAa;AACjB,MAAK,IAAI,QAAQ,MAAM,OAAO,SAAS,GAAG,QAExC,KADa,MAAM,KAAK,MAAM,CACpB,KAAK,KAAK,cAA4B,SAAS;AACvD,eAAa,MAAM,OAAO,MAAM;AAChC,eAAa;;AAIjB,KAAI,aAAa,KAAK,cAAc,EAClC,QAAO;CAGT,MAAM,QAAQ,MAAM,KAAK,aAAa,EAAE;CACxC,MAAM,QAAQ,MAAM,WAAW,aAAa,EAAE;CAC9C,MAAM,OAAO,eAAe,MAAM,eAAe,MAAM,CAAC;CACxD,MAAM,OAAO,MAAM,eAAe;AAElC,KAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,MAAM,eAAe,OAAO,OAAO,KAAK,CAC7D,QAAO;AAGT,KAAI,UAAU;EACZ,MAAM,MAAM,MAAM,MAAM,WAAW;EACnC,MAAM,KAAK,MAAM,GAAG,YAAY,KAAK,KAAK,KAAK;AAC/C,KAAG,aAAa,cAAc,KAAK,GAAG,IAAI,QAAQ,IAAI,EAAE,EAAE,CAAC;AAC3D,WAAS,GAAG,gBAAgB,CAAC;;AAE/B,QAAO;;;;AC3CT,SAAS,iBACP,QACA,KACA,KACA,QACA;CACA,MAAM,YAAY,YAAY,QAAQ,QAAQ;CAC9C,MAAM,eAAe,YAAY,QAAQ,WAAW;CACpD,MAAM,gBAAgB,YAAY,QAAQ,YAAY;CACtD,MAAM,sBAAsB,YAAY,QAAQ,kBAAkB;AAElE,KAAI,QAAQ;EAEV,MAAM,cAAc,OADD,oBAAoB,eAAe,EACf,IAAI;EAC3C,MAAM,YAAY,aAAa,cAAc,MAAM,YAAY;EAG/D,MAAM,YAAY,OADD,cAAc,eAAe,EACX,IAAI;EAEvC,MAAM,WAAW,OADD,aAAa,cAAc,MAAM,UAAU,EAC1B,MAAM,EAAE;AAEzC,SAAO,UAAU,cAAc,MAAM,CAAC,WAAW,GAAG,SAAS,CAAC;QACzD;EAEL,MAAM,YAAY,OADD,cAAc,eAAe,EACX,IAAI;EAEvC,MAAM,WAAW,OADD,aAAa,cAAc,MAAM,UAAU,EAC1B,IAAI;AAErC,SAAO,UAAU,cAAc,MAAM,SAAS;;;AAIlD,SAAS,OAAU,MAAS,QAAqB;AAC/C,QAAO,MAAS,OAAO,CAAC,KAAK,KAAK;;;;;;;;;;AAiCpC,SAAgB,YAAY,SAAsC;AAChE,SAAQ,OAAO,UAAU,SAAS;EAChC,MAAM,EAAE,KAAK,KAAK,SAAS,UAAU;AAErC,SAAO,WAAW,EAAE,MADN,iBAAiB,MAAM,QAAQ,KAAK,KAAK,OAAO,EAC7B,CAAC,CAAC,OAAO,UAAU,KAAK;;;;;;;;;;ACjE7D,SAAgB,gBAAgB,OAAwC;AACtE,QAAO,iBAAiB;;;;;;;ACS1B,SAAgB,gBAAgB,SAA2C;AACzE,SAAQ,OAAO,aAAa;EAC1B,MAAM,WAAW,YACf,MAAM,KACN,SAAS,OAAO,MAAM,UAAU,OACjC;AACD,MAAI,CAAC,SACH,QAAO;AAET,MAAI,UAAU;GACZ,MAAM,YAAY,IAAI,cAAc,SAAS;AAC7C,YAAS,MAAM,GAAG,aAAa,UAAU,CAAC;;AAE5C,SAAO;;;;;;;;ACPX,SAAgB,kBAAkB,SAA6C;AAC7E,SAAQ,OAAO,aAAa;EAC1B,MAAM,QAAQ,cAAc,MAAM,WAAW,SAAS,QAAQ,SAAS,KAAK;AAC5E,MAAI,CAAC,MACH,QAAO;AAET,MAAI,UAAU;GACZ,MAAM,CAAC,aAAa,aAAa;GACjC,MAAM,YAAY,cAAc,aAAa,aAAa,UAAU;AACpE,YAAS,MAAM,GAAG,aAAa,UAAU,CAAC;;AAE5C,SAAO;;;;;;;;ACXX,SAAgB,eAAe,SAA0C;AACvE,SAAQ,OAAO,aAAa;EAC1B,MAAM,QAAQ,cAAc,MAAM,WAAW,SAAS,QAAQ,SAAS,KAAK;AAC5E,MAAI,CAAC,MACH,QAAO;AAET,MAAI,UAAU;GACZ,MAAM,CAAC,aAAa,aAAa;GACjC,MAAM,YAAY,cAAc,aAAa,aAAa,UAAU;AACpE,YAAS,MAAM,GAAG,aAAa,UAAU,CAAC;;AAE5C,SAAO;;;;;;;;ACjBX,SAAgB,YAAY,SAAuC;AACjE,SAAQ,OAAO,aAAa;EAI1B,MAAM,QAAQ,UAHD,SAAS,MAClB,MAAM,IAAI,QAAQ,QAAQ,IAAI,GAC9B,MAAM,UAAU,QACS;AAC7B,MAAI,CAAC,MACH,QAAO;EAET,MAAM,MAAM,SAAS,IAAI,MAAM,KAAK;AACpC,MAAI,IAAI,IAAI,WAAW,EACrB,QAAO;AAET,MAAI,UAAU;GACZ,IAAI,KAAK,MAAM;GACf,MAAM,sBAAsB,IAAI,IAAI;GACpC,MAAM,qBAAqB,IAAI,IAAI,IAAI,IAAI,SAAS;GACpD,MAAM,eAAe,MAAM,MAAM,sBAAsB;GACvD,MAAM,cAAc,MAAM,MAAM,qBAAqB;GAGrD,MAAM,YAAY,IAAI,cAFA,GAAG,IAAI,QAAQ,aAAa,EAC7B,GAAG,IAAI,QAAQ,YAAY,CACgB;AAChE,QAAK,GAAG,aAAa,UAAU;AAC/B,cAAW,GAAG;;AAEhB,SAAO;;;;;;;;;;ACeX,SAAgB,sBAA8C;AAC5D,QAAO,eAAe;EACpB;EACA,iBAAiB;EAEjB;EACA;EACA;EACA;EAEA,4BAA4B;EAC5B,2BAA2B;EAC3B,wBAAwB;EACxB,wBAAwB;EAExB,mBAAmB;EACnB,yBAAyB;EACzB,sBAAsB;EACtB,2BAA2B;EAE3B,uBAAuB;EACvB,sBAAsB;EAEtB;EACA;EACD,CAAC;;;;;;;;;;ACzEJ,SAAgB,2BAA2C;AACzD,QAAO,oBAAoB,EACzB,QACD,CAAC;;AAGJ,MAAM,2BAAW,IAAI,SAAgC;AAErD,MAAM,UAA4B,EAAE,YAAqB;CACvD,MAAM,eAAe,MAAM;AAC3B,KAAI,CAAC,aAAc,QAAO;CAE1B,IAAI;AAEJ,KAAI,SAAS,IAAI,aAAa,CAC5B,SAAQ,SAAS,IAAI,aAAa;MAC7B;AAKL,UADc,aAAa,MACb,SAAS,2CAA2C;AAClE,WAAS,IAAI,cAAc,MAAM;;AAKnC,QAAO,CAAC;;;;;;;AChCV,SAAgB,qBAAqC;AACnD,QAAO,aAAa,CAAC,cAAc,EAAE,gBAAgB,CAAC,CAAC;;;;ACHzD,MAAM,cAAc;AAWpB,MAAM,YAAY;CAChB,SAAS,EAAE,SAAS,GAAG;CACvB,SAAS,EAAE,SAAS,GAAG;CACvB,UAAU,EAAE,SAAS,MAAM;CAC5B;AAWD,MAAM,QAAQ,WAAW;CACvB,YAAY;CACZ;CACA,gBAAgB,EAAE;CACnB,CAAC;;;;AAKF,SAAgB,kBAAsC;AACpD,QAAO,eAAe;EACpB,GAAG,MAAM;EACT,SAAS;EACT,MAAM;EACP,CAAC;;;;;AAeJ,SAAgB,qBAA4C;AAC1D,QAAO,eAAe;EACpB,GAAG,MAAM;EACT,SAAS;EACT,MAAM;EACP,CAAC;;;;;AAeJ,SAAgB,sBAA8C;AAC5D,QAAO,eAAe;EACpB,GAAG,MAAM;EACT,MAAM;EACN,OAAO;EACR,CAAC;;AAYJ,SAAgB,4BAA0D;AACxE,QAAO,eAAe;EACpB,GAAG,MAAM;EACT,MAAM;EACN,OAAO;EACR,CAAC;;;;;;;ACrEJ,SAAgB,cAA8B;AAC5C,QAAO,MACL,iBAAiB,EACjB,oBAAoB,EACpB,qBAAqB,EACrB,2BAA2B,EAC3B,oBAAoB,EACpB,qBAAqB,EACrB,0BAA0B,CAC3B"}
@@ -1,8 +1,5 @@
1
1
  .prosekit-virtual-selection {
2
- border-radius: 2px;
3
2
  background-color: #8888884d;
3
+ border-radius: 2px;
4
4
  box-shadow: 0 0 0 2px #8888884d;
5
5
  }
6
-
7
-
8
- /*# sourceMappingURL=style.css.map*/
@@ -1,34 +1,28 @@
1
1
  .ProseMirror .ProseMirror-yjs-cursor {
2
- position: absolute;
3
- height: 1em;
4
- border-left: black;
5
- border-left-width: 2px;
6
- border-left-style: solid;
7
- border-color: orange;
8
2
  word-break: normal;
9
3
  pointer-events: none;
4
+ border-color: orange;
5
+ border-left-style: solid;
6
+ border-left-width: 2px;
7
+ height: 1em;
8
+ position: absolute;
10
9
  }
11
10
 
12
11
  .ProseMirror .ProseMirror-yjs-cursor > div {
13
- position: relative;
14
- top: -1.05em;
15
- padding-right: 2px;
12
+ color: #fff;
13
+ user-select: none;
14
+ background-color: #fa8100;
16
15
  padding-left: 2px;
17
- background-color: rgb(250, 129, 0);
18
- color: white;
16
+ padding-right: 2px;
17
+ font-family: serif;
18
+ font-size: 13px;
19
19
  font-style: normal;
20
20
  font-weight: normal;
21
- font-size: 13px;
22
21
  line-height: normal;
23
- font-family: serif;
24
- -webkit-user-select: none;
25
- -moz-user-select: none;
26
- -ms-user-select: none;
27
- user-select: none;
22
+ position: relative;
23
+ top: -1.05em;
28
24
  }
25
+
29
26
  .ProseMirror > .ProseMirror-yjs-cursor:first-child {
30
27
  margin-top: 16px;
31
28
  }
32
-
33
-
34
- /*# sourceMappingURL=style.css.map*/
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@prosekit/extensions",
3
3
  "type": "module",
4
- "version": "0.14.2",
4
+ "version": "0.15.0",
5
5
  "private": false,
6
6
  "description": "A collection of common extensions for ProseKit",
7
7
  "author": {
@@ -146,6 +146,13 @@
146
146
  "types": "./dist/prosekit-extensions-mod-click-prevention.d.ts",
147
147
  "default": "./dist/prosekit-extensions-mod-click-prevention.js"
148
148
  },
149
+ "./page": {
150
+ "types": "./dist/prosekit-extensions-page.d.ts",
151
+ "default": "./dist/prosekit-extensions-page.js"
152
+ },
153
+ "./page/style.css": {
154
+ "default": "./dist/page/style.css"
155
+ },
149
156
  "./paragraph": {
150
157
  "types": "./dist/prosekit-extensions-paragraph.d.ts",
151
158
  "default": "./dist/prosekit-extensions-paragraph.js"
@@ -219,19 +226,20 @@
219
226
  "src"
220
227
  ],
221
228
  "dependencies": {
222
- "@ocavue/utils": "^1.5.0",
229
+ "@ocavue/utils": "^1.6.0",
223
230
  "prosemirror-changeset": "^2.4.0",
224
231
  "prosemirror-drop-indicator": "^0.1.3",
225
232
  "prosemirror-dropcursor": "^1.8.2",
226
233
  "prosemirror-enter-rules": "^0.1.5",
227
234
  "prosemirror-flat-list": "^0.5.8",
228
- "prosemirror-gapcursor": "^1.4.0",
229
- "prosemirror-highlight": "^0.15.0",
235
+ "prosemirror-gapcursor": "^1.4.1",
236
+ "prosemirror-highlight": "^0.15.1",
230
237
  "prosemirror-math": "^0.2.2",
231
238
  "prosemirror-search": "^1.1.0",
232
239
  "prosemirror-tables": "^1.8.5",
233
- "shiki": "^3.22.0",
234
- "@prosekit/core": "^0.10.0",
240
+ "server-dom-shim": "^1.1.0",
241
+ "shiki": "^3.22.0 || ^4.0.0",
242
+ "@prosekit/core": "^0.11.0",
235
243
  "@prosekit/pm": "^0.1.15"
236
244
  },
237
245
  "peerDependencies": {
@@ -257,8 +265,8 @@
257
265
  "devDependencies": {
258
266
  "diffable-html-snapshot": "^0.2.0",
259
267
  "just-pick": "^4.2.0",
260
- "katex": "^0.16.28",
261
- "loro-crdt": "^1.10.6",
268
+ "katex": "^0.16.40",
269
+ "loro-crdt": "^1.10.8",
262
270
  "loro-prosemirror": "^0.4.3",
263
271
  "rehype-parse": "^9.0.1",
264
272
  "rehype-remark": "^10.0.1",
@@ -266,17 +274,17 @@
266
274
  "remark-html": "^16.0.1",
267
275
  "remark-parse": "^11.0.0",
268
276
  "remark-stringify": "^11.0.0",
269
- "tsdown": "^0.20.3",
270
- "type-fest": "^5.4.4",
277
+ "tsdown": "^0.21.4",
278
+ "type-fest": "^5.5.0",
271
279
  "typescript": "~5.9.3",
272
280
  "unified": "^11.0.5",
273
- "vitest": "^4.1.0-beta.4",
281
+ "vitest": "^4.1.1",
274
282
  "vitest-browser-commands": "^0.2.0",
275
283
  "y-prosemirror": "^1.3.7",
276
- "yjs": "^13.6.29",
284
+ "yjs": "^13.6.30",
277
285
  "@prosekit/config-tsdown": "0.0.0",
278
- "@prosekit/config-vitest": "0.0.0",
279
- "@prosekit/config-ts": "0.0.0"
286
+ "@prosekit/config-ts": "0.0.0",
287
+ "@prosekit/config-vitest": "0.0.0"
280
288
  },
281
289
  "publishConfig": {
282
290
  "dev": {}
@@ -314,6 +322,8 @@
314
322
  "prosekit-extensions-math": "./src/math/index.ts",
315
323
  "prosekit-extensions-mention": "./src/mention/index.ts",
316
324
  "prosekit-extensions-mod-click-prevention": "./src/mod-click-prevention/index.ts",
325
+ "prosekit-extensions-page": "./src/page/index.ts",
326
+ "page/style": "./src/page/style.css",
317
327
  "prosekit-extensions-paragraph": "./src/paragraph/index.ts",
318
328
  "prosekit-extensions-paste-rule": "./src/paste-rule/index.ts",
319
329
  "prosekit-extensions-placeholder": "./src/placeholder/index.ts",
@@ -422,6 +432,9 @@
422
432
  "mod-click-prevention": [
423
433
  "./dist/prosekit-extensions-mod-click-prevention.d.ts"
424
434
  ],
435
+ "page": [
436
+ "./dist/prosekit-extensions-page.d.ts"
437
+ ],
425
438
  "paragraph": [
426
439
  "./dist/prosekit-extensions-paragraph.d.ts"
427
440
  ],
@@ -15,7 +15,8 @@ const insertHorizontalRuleCommand: Command = (state, dispatch): boolean => {
15
15
  const type = getNodeType(schema, 'horizontalRule')
16
16
  const node = type.createChecked()
17
17
  const pos = tr.selection.anchor
18
- tr.replaceRange(pos, pos, new Slice(Fragment.from(node), 0, 0))
18
+ const slice = new Slice(Fragment.from(node), 0, 0)
19
+ tr.replaceRange(pos, pos, slice).scrollIntoView()
19
20
  dispatch(tr)
20
21
  return true
21
22
  }
package/src/loro/loro.ts CHANGED
@@ -1,4 +1,5 @@
1
- import { Priority, union, withPriority, type PlainExtension, type Union } from '@prosekit/core'
1
+ import type { Priority } from '@prosekit/core'
2
+ import { union, withPriority, type PlainExtension, type Union } from '@prosekit/core'
2
3
  import type {
3
4
  CursorAwareness,
4
5
  CursorEphemeralStore,
@@ -65,6 +66,6 @@ export function defineLoro(options: LoroOptions): LoroExtension {
65
66
  defineLoroUndoPlugin({ ...undo, doc }),
66
67
  defineLoroSyncPlugin({ ...sync, doc }),
67
68
  ]),
68
- Priority.high,
69
+ 3 satisfies typeof Priority.high,
69
70
  )
70
71
  }
@@ -0,0 +1,5 @@
1
+ export { definePageBreakCommands, insertPageBreak, type PageBreakCommandsExtension } from './page-break-commands.ts'
2
+ export { definePageBreakKeymap, type PageBreakKeymapExtension } from './page-break-keymap.ts'
3
+ export { definePageBreakSpec, type PageBreakSpecExtension } from './page-break-spec.ts'
4
+ export { definePageBreak, type PageBreakExtension } from './page-break.ts'
5
+ export { definePageRendering, type PageRenderingExtension, type PageRenderingOptions } from './page-rendering.ts'
@@ -0,0 +1,61 @@
1
+ import { union } from '@prosekit/core'
2
+ import { describe, expect, it } from 'vitest'
3
+
4
+ import { defineDoc } from '../doc/index.ts'
5
+ import { defineHardBreak } from '../hard-break/index.ts'
6
+ import { defineHorizontalRule } from '../horizontal-rule/index.ts'
7
+ import { defineParagraph } from '../paragraph/index.ts'
8
+ import { setupTestFromExtension } from '../testing/index.ts'
9
+ import { defineText } from '../text/index.ts'
10
+
11
+ import { definePageBreak } from './page-break.ts'
12
+
13
+ function setup() {
14
+ const extension = union(
15
+ defineDoc(),
16
+ defineText(),
17
+ defineParagraph(),
18
+ defineHorizontalRule(),
19
+ defineHardBreak(),
20
+ definePageBreak(),
21
+ )
22
+ return setupTestFromExtension(extension)
23
+ }
24
+
25
+ describe('insertPageBreak', () => {
26
+ it('should insert a page break in an empty paragraph', () => {
27
+ const { editor, n } = setup()
28
+ editor.set(n.doc(n.paragraph('<a>')))
29
+ editor.commands.insertPageBreak()
30
+ expect(editor.view.state.doc.toJSON()).toEqual(
31
+ n.doc(n.pageBreak()).toJSON(),
32
+ )
33
+ })
34
+
35
+ it('should insert a page break after text', () => {
36
+ const { editor, n } = setup()
37
+ editor.set(n.doc(n.paragraph('hello<a>')))
38
+ editor.commands.insertPageBreak()
39
+ expect(editor.view.state.doc.toJSON()).toEqual(
40
+ n.doc(n.paragraph('hello'), n.pageBreak()).toJSON(),
41
+ )
42
+ })
43
+
44
+ it('should insert a page break before text', () => {
45
+ const { editor, n } = setup()
46
+ editor.set(n.doc(n.paragraph('<a>hello')))
47
+ editor.commands.insertPageBreak()
48
+ expect(editor.view.state.doc.toJSON()).toEqual(
49
+ n.doc(n.pageBreak(), n.paragraph('hello')).toJSON(),
50
+ )
51
+ })
52
+
53
+ it('should insert a page break between text', () => {
54
+ const { editor, n } = setup()
55
+ editor.set(n.doc(n.paragraph('hel<a>lo')))
56
+ editor.commands.insertPageBreak()
57
+ expect(editor.view.state.doc.toJSON()).toEqual(
58
+ n.doc(n.paragraph('hel'), n.pageBreak(), n.paragraph('lo')).toJSON(),
59
+ )
60
+ })
61
+ })
@@ -0,0 +1,41 @@
1
+ import { defineCommands, getNodeType, type Extension } from '@prosekit/core'
2
+ import { Fragment, Slice } from '@prosekit/pm/model'
3
+ import type { Command } from '@prosekit/pm/state'
4
+
5
+ /**
6
+ * @internal
7
+ */
8
+ export type PageBreakCommandsExtension = Extension<{
9
+ Commands: {
10
+ insertPageBreak: []
11
+ }
12
+ }>
13
+
14
+ const insertPageBreakCommand: Command = (state, dispatch): boolean => {
15
+ if (!dispatch) return true
16
+
17
+ const { schema, tr } = state
18
+ const type = getNodeType(schema, 'pageBreak')
19
+ const node = type.createChecked()
20
+ const pos = tr.selection.anchor
21
+ const slice = new Slice(Fragment.from(node), 0, 0)
22
+ tr.replaceRange(pos, pos, slice).scrollIntoView()
23
+ dispatch(tr)
24
+ return true
25
+ }
26
+
27
+ /**
28
+ * @internal
29
+ */
30
+ export function insertPageBreak(): Command {
31
+ return insertPageBreakCommand
32
+ }
33
+
34
+ /**
35
+ * @internal
36
+ */
37
+ export function definePageBreakCommands(): PageBreakCommandsExtension {
38
+ return defineCommands({
39
+ insertPageBreak: insertPageBreak,
40
+ })
41
+ }
@@ -0,0 +1,17 @@
1
+ import { defineKeymap, type PlainExtension } from '@prosekit/core'
2
+
3
+ import { insertPageBreak } from './page-break-commands.ts'
4
+
5
+ /**
6
+ * @internal
7
+ */
8
+ export type PageBreakKeymapExtension = PlainExtension
9
+
10
+ /**
11
+ * @internal
12
+ */
13
+ export function definePageBreakKeymap(): PageBreakKeymapExtension {
14
+ return defineKeymap({
15
+ 'Mod-Enter': insertPageBreak(),
16
+ })
17
+ }
@@ -0,0 +1,33 @@
1
+ import { defineNodeSpec, type Extension } from '@prosekit/core'
2
+ import type { Attrs } from '@prosekit/pm/model'
3
+
4
+ /**
5
+ * @internal
6
+ */
7
+ export type PageBreakSpecExtension = Extension<{
8
+ Nodes: {
9
+ pageBreak: Attrs
10
+ }
11
+ }>
12
+
13
+ /**
14
+ * @internal
15
+ */
16
+ export function definePageBreakSpec(): PageBreakSpecExtension {
17
+ return defineNodeSpec({
18
+ name: 'pageBreak',
19
+ group: 'block',
20
+ selectable: true,
21
+ parseDOM: [{ tag: 'div.prosekit-page-break' }],
22
+ toDOM() {
23
+ return ['div', { class: 'prosekit-horizontal-rule prosekit-page-break' }, ['hr']]
24
+ },
25
+ pageBreak: true,
26
+ })
27
+ }
28
+
29
+ declare module '@prosekit/pm/model' {
30
+ interface NodeSpec {
31
+ pageBreak?: boolean | undefined
32
+ }
33
+ }
@@ -0,0 +1,23 @@
1
+ import { union, type Union } from '@prosekit/core'
2
+
3
+ import { definePageBreakCommands, type PageBreakCommandsExtension } from './page-break-commands.ts'
4
+ import { definePageBreakKeymap, type PageBreakKeymapExtension } from './page-break-keymap.ts'
5
+ import { definePageBreakSpec, type PageBreakSpecExtension } from './page-break-spec.ts'
6
+
7
+ /**
8
+ * @internal
9
+ */
10
+ export type PageBreakExtension = Union<
11
+ [PageBreakSpecExtension, PageBreakCommandsExtension, PageBreakKeymapExtension]
12
+ >
13
+
14
+ /**
15
+ * @public
16
+ */
17
+ export function definePageBreak(): PageBreakExtension {
18
+ return union(
19
+ definePageBreakSpec(),
20
+ definePageBreakCommands(),
21
+ definePageBreakKeymap(),
22
+ )
23
+ }
@@ -0,0 +1,246 @@
1
+ import { once } from '@ocavue/utils'
2
+ import { customElements, HTMLElement } from 'server-dom-shim'
3
+
4
+ /**
5
+ * @internal
6
+ */
7
+ export const PAGE_CHUNK_TAG_NAME = 'pm-page-chunk'
8
+
9
+ /**
10
+ * @internal
11
+ */
12
+ export const registerPageChunkElement: VoidFunction = /* @__PURE__ */ once(() => {
13
+ if (typeof window === 'undefined' || customElements.get(PAGE_CHUNK_TAG_NAME)) return
14
+ customElements.define(PAGE_CHUNK_TAG_NAME, PageChunkElement)
15
+ })
16
+
17
+ class PageChunkElement extends HTMLElement {
18
+ static observedAttributes = [
19
+ 'data-group',
20
+ 'data-break',
21
+ 'data-h',
22
+ 'data-mt',
23
+ 'data-mb',
24
+
25
+ // Only the first chunk of the whole document has this attribute.
26
+ 'data-size',
27
+ ]
28
+
29
+ // Data attributes set by external code
30
+ #group: string = ''
31
+ #forceNextBreak: boolean = false
32
+ #pageHeight: number = 0
33
+ #pageMarginTop: number = 0
34
+ #pageMarginBottom: number = 0
35
+ #size: number | undefined = undefined
36
+
37
+ // Internal states
38
+ #updateRequested: boolean = false
39
+ #contentBoxHeight: number = 0
40
+
41
+ // Rendering states
42
+ #isHead: boolean = false
43
+ #isTail: boolean = false
44
+ #paddingTop: number = 0
45
+ #paddingBottom: number = 0
46
+
47
+ // Pending rendering states
48
+ #isHeadPending: boolean = false
49
+ #isTailPending: boolean = false
50
+ #paddingTopPending: number = 0
51
+ #paddingBottomPending: number = 0
52
+
53
+ connectedCallback() {
54
+ this.#parseDataAttributes()
55
+
56
+ if (this.#isLeader()) {
57
+ this.#isHeadPending = true
58
+ }
59
+
60
+ this.#render()
61
+
62
+ // Get the initial content box height when the resize observer is not started yet. Notice that
63
+ // `this.clientHeight` is an integer while the content box height can be a float, so this is not
64
+ // accurate but should be good enough for the first render.
65
+ this.#contentBoxHeight = this.clientHeight - this.#paddingTop - this.#paddingBottom
66
+
67
+ observeElement(this)
68
+
69
+ this.#requestUpdate()
70
+ }
71
+
72
+ disconnectedCallback() {
73
+ unobserveElement(this)
74
+ }
75
+
76
+ attributeChangedCallback(_: string, oldValue: string | null, newValue: string | null) {
77
+ if (oldValue === newValue) return
78
+ this.#parseDataAttributes()
79
+ this.#requestUpdate()
80
+ }
81
+
82
+ #parseDataAttributes() {
83
+ this.#group = this.getAttribute('data-group') || ''
84
+ this.#forceNextBreak = this.hasAttribute('data-break')
85
+ this.#pageHeight = this.#parseFloatAttribute('data-h')
86
+ this.#pageMarginTop = this.#parseFloatAttribute('data-mt')
87
+ this.#pageMarginBottom = this.#parseFloatAttribute('data-mb')
88
+
89
+ const sizeAttr = this.getAttribute('data-size')
90
+ this.#size = sizeAttr ? Number.parseInt(sizeAttr, 10) : undefined
91
+ }
92
+
93
+ #parseFloatAttribute(name: string): number {
94
+ const value = this.getAttribute(name)
95
+ return value != null ? Number.parseFloat(value) : 0
96
+ }
97
+
98
+ #isLeader() {
99
+ return this.#size != null
100
+ }
101
+
102
+ #render() {
103
+ if (this.#paddingTop !== this.#paddingTopPending || this.#paddingBottom !== this.#paddingBottomPending) {
104
+ Object.assign(this.style, {
105
+ paddingTop: `${this.#paddingTop = this.#paddingTopPending}px`,
106
+ paddingBottom: `${this.#paddingBottom = this.#paddingBottomPending}px`,
107
+ })
108
+ }
109
+ if (this.#isHead !== this.#isHeadPending) {
110
+ this.toggleAttribute('data-page-head', this.#isHead = this.#isHeadPending)
111
+ }
112
+ if (this.#isTail !== this.#isTailPending) {
113
+ this.toggleAttribute('data-page-tail', this.#isTail = this.#isTailPending)
114
+ }
115
+ }
116
+
117
+ setHeight(height: number) {
118
+ // Avoid potential float number precision issues
119
+ if (Math.abs(this.#contentBoxHeight - height) < 0.1) {
120
+ return
121
+ }
122
+ this.#contentBoxHeight = height
123
+ this.#requestUpdate()
124
+ }
125
+
126
+ /**
127
+ * Schedules a batched page layout recalculation.
128
+ *
129
+ * Any chunk can call this method, but the actual layout work (#updateAll)
130
+ * always runs on the leader chunk, because it needs to iterate over every
131
+ * chunk in order to compute page breaks.
132
+ *
133
+ * Two nested microtasks are used to batch updates:
134
+ *
135
+ * Microtask 1 – Delegation: non-leader chunks forward the request to the
136
+ * leader chunk, so multiple chunks changing in the same tick only trigger
137
+ * one layout pass.
138
+ *
139
+ * Microtask 2 – Execution: the leader chunk defers #updateAll to a second
140
+ * microtask so that any other attribute / resize changes that were queued
141
+ * in the same tick (and forwarded during microtask 1) are already reflected
142
+ * before the layout is recalculated.
143
+ *
144
+ * The #updateRequested flag acts as a deduplication guard so that rapid
145
+ * successive calls (e.g. multiple attributes changing at once) result in at
146
+ * most one scheduled pass per chunk.
147
+ */
148
+ #requestUpdate() {
149
+ if (this.#updateRequested) {
150
+ return
151
+ }
152
+
153
+ this.#updateRequested = true
154
+ queueMicrotask(() => {
155
+ if (!this.#isLeader()) {
156
+ this.#updateRequested = false
157
+ const leader = findLeaderChunk(this, this.#group)
158
+ if (!leader) return
159
+ leader.#requestUpdate()
160
+ return
161
+ }
162
+ queueMicrotask(() => {
163
+ this.#updateRequested = false
164
+ this.#updateAll()
165
+ })
166
+ })
167
+ }
168
+
169
+ #updateAll() {
170
+ if (!this.isConnected) {
171
+ return
172
+ }
173
+
174
+ const elements = findAllChunks(this, this.#group)
175
+ const count = elements.length
176
+ if (count === 0) return
177
+
178
+ const pageHeight = this.#pageHeight
179
+ const pageMarginTop = this.#pageMarginTop
180
+ const maxContentHeight = pageHeight - pageMarginTop - this.#pageMarginBottom
181
+
182
+ let currentContentHeight = 0
183
+ let forceNextBreak = false
184
+
185
+ for (let i = 0; i < count; i++) {
186
+ const element = elements[i]
187
+ const h = element.#contentBoxHeight
188
+ const isHead = forceNextBreak || i === 0 || (currentContentHeight + h > maxContentHeight)
189
+
190
+ forceNextBreak = element.#forceNextBreak
191
+
192
+ if (isHead && i > 0) {
193
+ const prev = elements[i - 1]
194
+ prev.#paddingBottomPending = Math.max(0, pageHeight - pageMarginTop - currentContentHeight)
195
+ prev.#isTailPending = true
196
+ currentContentHeight = h
197
+ } else {
198
+ currentContentHeight += h
199
+ }
200
+
201
+ element.#paddingTopPending = isHead ? pageMarginTop : 0
202
+ element.#paddingBottomPending = 0
203
+ element.#isTailPending = false
204
+ element.#isHeadPending = isHead
205
+ }
206
+
207
+ const last = elements[count - 1]
208
+ last.#paddingBottomPending = Math.max(0, pageHeight - pageMarginTop - currentContentHeight)
209
+ last.#isTailPending = true
210
+
211
+ for (const element of elements) {
212
+ element.#render()
213
+ }
214
+ }
215
+ }
216
+
217
+ function handleResize(entries: ResizeObserverEntry[]) {
218
+ for (const entry of entries) {
219
+ const contentBoxHeight = entry.contentBoxSize?.[0]?.blockSize ?? entry.contentRect.height
220
+ const element = entry.target as PageChunkElement
221
+ element.setHeight(contentBoxHeight)
222
+ }
223
+ }
224
+
225
+ const getResizeObserver = /* @__PURE__ */ once(() => {
226
+ return new ResizeObserver(handleResize)
227
+ })
228
+
229
+ function observeElement(element: PageChunkElement) {
230
+ getResizeObserver().observe(element)
231
+ }
232
+
233
+ function unobserveElement(element: PageChunkElement) {
234
+ getResizeObserver().unobserve(element)
235
+ }
236
+
237
+ function findLeaderChunk(element: HTMLElement, group: string): PageChunkElement | null | undefined {
238
+ const root = element.closest('.ProseMirror')
239
+ return root?.querySelector<PageChunkElement>(`${PAGE_CHUNK_TAG_NAME}[data-group="${group}"][data-size]`)
240
+ }
241
+
242
+ function findAllChunks(element: HTMLElement, group: string): PageChunkElement[] {
243
+ const root = element.closest('.ProseMirror')
244
+ const elements = root?.querySelectorAll<PageChunkElement>(`${PAGE_CHUNK_TAG_NAME}[data-group="${group}"]`)
245
+ return Array.from(elements || [])
246
+ }