Skip to content

feat(muya): add createTable / insertImage / setCursor API - #4397

Merged
Jocs merged 2 commits into
developfrom
feat/muya-create-table-image-cursor
Jun 8, 2026
Merged

Jocs merged 2 commits into
developfrom
feat/muya-create-table-image-cursor

Conversation

@Jocs

@Jocs Jocs commented Jun 8, 2026

Copy link
Copy Markdown
Member

Motivation

The desktop app is migrating off the legacy packages/muyajs engine onto
packages/muya (@muyajs/core). The block-editing API the desktop menus
drive (duplicate / insertParagraph / deleteParagraph / updateParagraph)
already landed in develop; this PR adds the remaining three methods so the
Table/Image/Cursor desktop actions have a public entrypoint on @muyajs/core.

All three are added on Muya (packages/muya/src/muya.ts), adjacent to the
existing deleteParagraph().

What's added

createTable({ rows, columns })

Builds the equivalent muya ITableState of the legacy createTableInFigure:
rows total rows × columns columns, the first row as the header, every row a
table.row with table.cell children (meta.align: 'none', empty text).
Creates it via ScrollPage.loadBlock('table').create(...) and replaces the
block at the cursor (_outmostBlockAtCursor() → block.replaceWith(...)),
placing the caret in the first cell. Matches muya's replaceBlockByLabel('table')
/ Table.create conventions.

insertImage({ src, alt })

Inserts an inline image at the cursor in the active Format block. The
![alt](src) markdown is written through the block's text setter so it
dispatches a JSON op (state stays in sync) rather than bypassing state sync.
Derives alt from the file name when none is given and percent-encodes plain
paths (leaving full URLs / data URLs intact), mirroring legacy insertImage
and muya's replaceImage. No-ops safely when there is no active formattable
block (e.g. inside a code block, or no cursor).

setCursor(cursor)

Sets the cursor programmatically. Resolves the target block(s) by path on the
live tree (editor.scrollPage.queryBlock(path)) and restores the selection the
same way Editor.updateContents does: block.setCursor(begin, end, true) for
the same-block case, falling back to editor.selection.setSelection({...}) with
resolved block instances for cross-block (passing bare paths does not work —
Selection._setCursor needs a block's domNode). Accepts both the
{ anchor, focus, anchorPath, focusPath } and { start, end, path } / block
ICursor shapes.

Tests

packages/muya/src/__tests__/createTableImageCursor.spec.ts (happy-dom,
state flushes on rAF → vi.waitFor):

  • createTable: correct dimensions, empty cells with align: 'none', caret in
    the first cell, no-op without a current block.
  • insertImage: image markdown in the document (getMarkdown contains the
    src), alt derivation, explicit alt, space percent-encoding, no-op without an
    active formattable block.
  • setCursor: caret positioned in the same block (both ICursor shapes),
    cross-block resolution, and a no-op for an unresolvable path.

Verification

All green from packages/muya:

  • pnpm lint — 0 errors (pre-existing warnings only)
  • pnpm lint:types — clean
  • pnpm check-circular — no circular dependency
  • pnpm test — 454 passed (+13)
  • pnpm test:spec — 1347 passed

🤖 Generated with Claude Code

Complete the programmatic block-editing surface @muyajs/core exposes for
the desktop migration off legacy packages/muyajs, alongside the existing
duplicate / insertParagraph / deleteParagraph / updateParagraph methods.

- createTable({ rows, columns }): builds a GFM ITableState (first row is
  the header, every cell empty with align 'none') and replaces the block
  at the cursor with it, placing the caret in the first cell. Mirrors
  legacy createTableInFigure/createFigure and muya's table conventions.
- insertImage({ src, alt }): writes ![alt](src) at the cursor in the
  active Format block through the text setter so it dispatches a JSON op
  (state stays in sync); derives alt from the file name and percent-
  encodes plain paths the way legacy insertImage / replaceImage do.
  No-ops when there is no active formattable block.
- setCursor(cursor): resolves the target block(s) by path on the live
  tree and restores the selection the way Editor.updateContents does —
  block.setCursor for same-block, selection.setSelection with resolved
  block instances for cross-block (bare paths don't work — _setCursor
  needs a block's domNode). Accepts the anchor/focus/path and start/end
  ICursor shapes.

Adds a happy-dom vitest spec covering dimensions, cell defaults, caret
placement, image markdown/alt/encoding, and same/cross-block cursor
resolution plus the no-op guards.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings June 8, 2026 07:24

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR expands the @muyajs/core (packages/muya) public, programmatic editing surface to support the remaining desktop menu actions needed for the migration off legacy packages/muyajs: table insertion, inline image insertion, and programmatic cursor placement.

Changes:

  • Add Muya.createTable({ rows, columns }) to replace the current block with a table and move the caret into the first cell.
  • Add Muya.insertImage({ src, alt }) to insert inline image markdown via a Format block text update (JSON op-backed).
  • Add Muya.setCursor(cursor) to restore selection by resolving blocks from paths (same-block vs cross-block).
  • Add a new Vitest happy-dom spec covering the new APIs.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
packages/muya/src/muya.ts Adds the new public APIs (createTable, insertImage, setCursor) on Muya.
packages/muya/src/tests/createTableImageCursor.spec.ts Adds unit tests covering the new editing APIs under happy-dom.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread packages/muya/src/muya.ts
Comment on lines +477 to +481
createTable({ rows, columns }: { rows: number; columns: number }) {
const block = this._outmostBlockAtCursor();
if (!block)
return;

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — fixed in cd6cb7a. createTable now coerces rows/columns to integers and clamps them to a valid GFM shape (rows >= 2 for header + at least one body row, columns >= 1): Math.max(2, Number.isFinite(rows) ? Math.floor(rows) : 0) and the column equivalent. So rows: 0, NaN, Infinity, and fractional/negative values now yield a usable table instead of an empty-row state that would crash Table.columnCount (which reads firstChild.firstChild). Added spec cases for {rows: 0, columns: 0}, non-finite, and fractional dimensions.

Comment thread packages/muya/src/muya.ts Outdated
Comment on lines +525 to +533
// Only percent-encode plain paths; leave full URLs / data URLs as-is.
// Mirrors legacy `insertImage` / `replaceImage` src handling.
let imgUrl: string;
if (URL_REG.test(src))
imgUrl = encodeURI(src);
else if (/^data:image\//.test(src))
imgUrl = src;
else
imgUrl = src.replace(/ /g, encodeURI(' ')).replace(/#/g, encodeURIComponent('#'));

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in cd6cb7a. The loose /^data:image\// check is replaced with the strict DATA_URL_REG (/^data:image\/[\w+-]+(?:;[\w-]+=[\w-]+|;base64)*,[a-zA-Z0-9+/]+={0,2}$/), which is the exact regex legacy muyajs insertImage/imageCtrl and packages/muya/src/utils/image.ts getImageSrc already use. I promoted it to config/index.ts so it's reused rather than duplicated. A bare/malformed data:image/... src no longer embeds verbatim; it falls through to the percent-encoded plain-path branch (matching legacy). Added spec cases for a well-formed base64 data URL (embedded verbatim) vs a malformed data: src (not embedded verbatim).

…ta-URL check

Address Copilot review on the createTable / insertImage / setCursor API:

- createTable now coerces rows/columns to integers and clamps to a valid
  GFM shape (rows >= 2, columns >= 1). Garbage input (rows: 0, NaN,
  Infinity, fractional/negative values) previously built an invalid table
  state and could crash Table.columnCount (which reads firstChild.firstChild).

- insertImage now uses the strict DATA_URL_REG instead of a loose
  /^data:image// prefix check, aligning with legacy muyajs insertImage and
  utils/image.ts getImageSrc. A bare/malformed data:image src no longer
  embeds verbatim; it falls through to the percent-encoded plain-path branch.
  Promoted the regex to config/index.ts for reuse.

Extends createTableImageCursor.spec.ts with clamping/coercion cases and
well-formed vs malformed data-URL cases.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@Jocs
Jocs merged commit 945014c into develop Jun 8, 2026
11 checks passed
@Jocs
Jocs deleted the feat/muya-create-table-image-cursor branch June 8, 2026 08:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants