Skip to content

feat(widget-markdown): support inline custom editor components - #7958

Open
mmaoteacher wants to merge 12 commits into
decaporg:mainfrom
mmaoteacher:5065/inline-editor-components
Open

feat(widget-markdown): support inline custom editor components#7958
mmaoteacher wants to merge 12 commits into
decaporg:mainfrom
mmaoteacher:5065/inline-editor-components

Conversation

@mmaoteacher

Copy link
Copy Markdown

Summary

Closes #5065
Relates to #2064

Motivation & Background

Currently, CMS.registerEditorComponent assumes all registered custom editor components are block-level elements. When developers attempt to register inline shortcode patterns (e.g., Hugo shortcodes {{< ref "..." >}}, Obsidian wikilinks [[wikilink]], inline badges, tags, or mentions):

  1. Rich Text Formatting Breakdown: The Slate editor forces these elements into standalone block nodes, breaking surrounding paragraph continuity and adding unwanted line breaks.
  2. Round-trip Serialization Errors: Reverse serialization (Slate -> MDAST -> Markdown) frequently triggers Sent invalid data to remark errors or strips leading/trailing whitespaces.
  3. Lack of Selection & Async Lifecycle: There is no native mechanism to wrap selected text into an inline element asynchronously (e.g., picking a link target via a modal or fetching external metadata).

Proposed Solution

This PR introduces first-class support for Inline Custom Editor Components in packages/decap-cms-widget-markdown via CMS.registerEditorComponent.

1. API Specification (CMS.registerEditorComponent)

We extend EditorComponentOptions with an explicit type: 'inline' discriminator alongside dedicated serialization and lifecycle hooks:

interface InlineEditorComponentOptions {
  id: string;                                                  // Unique identifier
  label: string;                                               // Toolbar button label / tooltip
  type: 'inline';                                              // Explicitly marks component as inline
  isVoid?: boolean;                                            // Atomicity flag (default: true; non-editable content)
  trigger?: string;                                            // Optional prefix character for Remark tokenizer optimization (e.g. '@', '[')
  
  // 1. Markdown Parsing & Stringification
  pattern: RegExp;                                             // Non-greedy regular expression matching the inline syntax
  fromInline: (match: RegExpExecArray) => Record<string, any>; // Parse matched regex into pure data object
  toInline: (data: Record<string, any>) => string;             // Serialize data object back to Markdown string
  
  // 2. Rich Text Visual Editor Rendering
  toPreview: (data: Record<string, any>) => React.ReactNode;
  
  // 3. Interactive & Async Lifecycles (Optional)
  onInsert?: (context: { 
    selectedText: string; 
    cmsContext: any; 
  }) => Promise<Record<string, any> | null>;                    // Returns data object, or null to abort insertion
  
  onEdit?: (context: { 
    data: Record<string, any>; 
  }) => Promise<Record<string, any> | null>;                    // Triggered when an existing inline node is clicked/edited
}

Backward Compatibility: Existing block-level components using fromBlock / toBlock without type: 'inline' remain completely unaffected and default to type: 'block'.


2. Architecture & Data Flow

Markdown (Raw Source)
  │ ▲
  ▼ │  [Remark inlineTokenizer / MDAST Stringifier]
MDAST Inline Node (`type: 'inline-shortcode'`)
  │ ▲
  ▼ │  [remarkSlate / slateRemark]
Slate Inline Element (`inline: true`, `void: isVoid`)
  │ ▲
  ▼ │  [Slate Element Component & renderers.js]
Rich Text Visual DOM (Rendered via `toPreview` with contentEditable={false})

3. Key Module Changes

  • packages/decap-cms-widget-markdown/src/serializers/remarkShortcodes.js:
    • Implemented inlineTokenizers.inlineShortcode with proper locator function for fast scanning inside paragraphs without breaking inline text parsing.
    • Added greedy regex warning in development mode.
  • packages/decap-cms-widget-markdown/src/serializers/slateRemark.js & remarkSlate.js:
    • Transformed MDAST inline-shortcode to Slate inline element nodes and vice versa.
    • Preserved inline continuity without injecting block delimiters (\n\n) around inline nodes.
  • packages/decap-cms-widget-markdown/src/MarkdownControl/plugins/shortcodes/:
    • Updated withShortcodes.js to register editor.isInline for inline-shortcode and support isVoid.
    • Updated insertShortcode.js to handle inline components with selection capture and async onInsert.
  • packages/decap-cms-widget-markdown/src/MarkdownControl/components/InlineShortcode.js:
    • Implemented React visual component renderer with contentEditable={false} for void nodes, selection highlight, and async onEdit trigger.

Scope & Future Work

  • Scope of this PR: This implementation specifically targets packages/decap-cms-widget-markdown, which is the primary and default Markdown editor used across Decap CMS.
  • Future Work: Support for packages/decap-cms-widget-richtext (based on Plate.js) will be tracked and implemented in a separate follow-up PR to keep this PR focused, lightweight, and risk-contained for reviewers.

Implementation Phases

  • Phase 0: RFC & Type Definitions
    • Create RFC specification (docs/specs/inline-editor-components-rfc.md).
    • Add TypeScript definitions to packages/decap-cms-core/index.d.ts.
  • Phase 1: Tokenizer & MDAST
    • Implement Remark inlineTokenizer supporting custom regex patterns.
    • Validate bidirectional round-trip conversions (Markdown <-> MDAST) without character escaping or whitespace loss.
  • Phase 2: Slate Integration
    • Register Slate Inline & Void node types.
    • Implement React visual component renderer with contentEditable={false} for void nodes.
  • Phase 3: Interactive Events & Toolbar
    • Add toolbar insertion workflow with selectedText capture and onInsert resolution.
    • Support double-click / click on existing inline nodes to trigger onEdit.
  • Phase 4: Tests & Edge Cases
    • Add round-trip unit tests for CJK characters and punctuation adjacent to inline shortcodes.
    • Add dev-time regex validation / warning for greedy expressions without boundary constraints.

Test plan

  1. Unit Tests (jest):

    npx jest packages/decap-cms-widget-markdown
    • Passed all unit tests in remarkShortcodes.spec.js, slate.spec.js, and insertShortcode.spec.js.
    • Verified CJK characters adjacent to inline syntax: 這是一個[[測試頁面]],請點擊!.
    • Verified nested styling inside paragraphs: **Important [badge:NEW] Note**.
  2. E2E Browser Tests (cypress):

    npx cypress run --browser electron --spec "cypress/e2e/markdown_widget_inline_component_spec.js"
    • Verified component insertion, live visual preview rendering, raw mode toggle, and edit modal lifecycle.
  3. Lint & Build:

    npx eslint packages/decap-cms-widget-markdown/src
    npm run build:esm --prefix packages/decap-cms-widget-markdown

Checklist

  • I have read the contribution guidelines.
  • Code follows project conventions and styling.
  • Tests have been added / updated.
  • Documentation updates are prepared.

@mmaoteacher
mmaoteacher requested a review from a team as a code owner August 15, 2026 10:07
@netlify

netlify Bot commented Aug 19, 2026

Copy link
Copy Markdown

Deploy Preview for decap-cms ready!

Name Link
🔨 Latest commit 83a6f08
🔍 Latest deploy log https://app.netlify.com/projects/decap-cms/deploys/6a8ab6d69db6360008227440
😎 Deploy Preview https://deploy-preview-7958--decap-cms.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

mmaoteacher and others added 3 commits August 20, 2026 08:08
* fix: solve flaky/failing tests

* fix: change config and cypress setup

* fix: increase test timeout

* feat: add sharding again

* fix: remaining tests (attempt)

* fix: ignore parameter order and clear isFetching
Bumps [nx](https://github.com/nrwl/nx/tree/HEAD/packages/nx) from 21.6.11 to 23.1.1.
- [Release notes](https://github.com/nrwl/nx/releases)
- [Commits](https://github.com/nrwl/nx/commits/23.1.1/packages/nx)

---
updated-dependencies:
- dependency-name: nx
  dependency-version: 23.1.1
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
@mmaoteacher
mmaoteacher force-pushed the 5065/inline-editor-components branch from f1c4b77 to 8e6e492 Compare August 20, 2026 00:09
@mmaoteacher

Copy link
Copy Markdown
Author

Hi @yanthomasdev ,

I figured out why CI failed — I forgot to register my component in dev-test/backends/test/index.html as well. I've fixed this, rebased, and force-pushed to the feature branch. Please re-run the CI tests when you have a chance.

Thanks!

@mmaoteacher
mmaoteacher force-pushed the 5065/inline-editor-components branch from 7d2b558 to 83a6f08 Compare August 23, 2026 09:01
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.

Inline custom widgets

3 participants