Skip to content

feat: language picker for code blocks#1176

Merged
ascorbic merged 2 commits into
mainfrom
code-blocks-language-picker
May 26, 2026
Merged

feat: language picker for code blocks#1176
ascorbic merged 2 commits into
mainfrom
code-blocks-language-picker

Conversation

@ascorbic
Copy link
Copy Markdown
Collaborator

@ascorbic ascorbic commented May 26, 2026

What does this PR do?

Adds an inline language picker to code blocks in both the admin rich text editor and the inline visual editor.

  • Hover any code block to reveal a small chip in the top-right corner
  • Click it to open a popover with a free-form input and a curated suggestions datalist (~30 languages: TypeScript, Python, Rust, Astro, SQL, Bash, etc.)
  • Aliases normalize on commit: typing ts stores typescript, c++ stores cpp, md stores markdown, etc.
  • The existing triple-backtick markdown shortcut continues to pre-populate the language (```html + space creates a code block already set to HTML)
  • Storage and frontend rendering are unchanged: the language attribute round-trips through Portable Text and emits as language-{id} on the rendered <pre> / <code>, ready for any frontend highlighter

Configurable syntax highlighting will land in a follow-up PR. This one just wires up the storage path end-to-end.

Type of change

  • Feature

Checklist

  • I have read CONTRIBUTING.md
  • pnpm typecheck passes
  • pnpm lint passes
  • pnpm test passes (908 admin tests, 3453 core tests)
  • pnpm format has been run
  • I have added/updated tests for my changes (16 new tests covering alias resolution, normalization, node view round-trip)
  • User-visible strings in the admin UI are wrapped for translation
  • I have added a changeset

AI-generated code disclosure

  • This PR includes AI-generated code — model/tool: Claude Opus 4.7

Screenshots


Try this PR

Open a fresh playground →

A full working EmDash site, deployed from this branch. Each visit gets its own session-scoped sandbox: no login needed and no shared state. Try the admin, edit content, hit the public site.

Tracks code-blocks-language-picker. Updated automatically when the playground redeploys.

Copilot AI review requested due to automatic review settings May 26, 2026 13:54
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 26, 2026

🦋 Changeset detected

Latest commit: ef0fcb8

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 14 packages
Name Type
@emdash-cms/admin Minor
emdash Minor
@emdash-cms/cloudflare Minor
@emdash-cms/sandbox-workerd Patch
@emdash-cms/fixture-perf-site Patch
@emdash-cms/perf-demo-site Patch
@emdash-cms/cache-demo-site Patch
@emdash-cms/auth Minor
@emdash-cms/blocks Minor
@emdash-cms/gutenberg-to-portable-text Minor
@emdash-cms/x402 Minor
create-emdash Minor
@emdash-cms/auth-atproto Patch
@emdash-cms/plugin-embeds Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions
Copy link
Copy Markdown
Contributor

Scope check

This PR changes 811 lines across 12 files. Large PRs are harder to review and more likely to be closed without review.

If this scope is intentional, no action needed. A maintainer will review it. If not, please consider splitting this into smaller PRs.

See CONTRIBUTING.md for contribution guidelines.

@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages Bot commented May 26, 2026

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
docs ef0fcb8 May 26 2026, 03:44 PM

@github-actions
Copy link
Copy Markdown
Contributor

PR template validation failed

Please fix the following issues by editing your PR description:

See CONTRIBUTING.md for the full contribution policy.

@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages Bot commented May 26, 2026

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
emdash-demo-cache ef0fcb8 May 26 2026, 03:44 PM

@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages Bot commented May 26, 2026

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
emdash-playground ef0fcb8 May 26 2026, 03:44 PM

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented May 26, 2026

Open in StackBlitz

@emdash-cms/admin

npm i https://pkg.pr.new/@emdash-cms/admin@1176

@emdash-cms/auth

npm i https://pkg.pr.new/@emdash-cms/auth@1176

@emdash-cms/blocks

npm i https://pkg.pr.new/@emdash-cms/blocks@1176

@emdash-cms/cloudflare

npm i https://pkg.pr.new/@emdash-cms/cloudflare@1176

emdash

npm i https://pkg.pr.new/emdash@1176

create-emdash

npm i https://pkg.pr.new/create-emdash@1176

@emdash-cms/gutenberg-to-portable-text

npm i https://pkg.pr.new/@emdash-cms/gutenberg-to-portable-text@1176

@emdash-cms/x402

npm i https://pkg.pr.new/@emdash-cms/x402@1176

@emdash-cms/plugin-ai-moderation

npm i https://pkg.pr.new/@emdash-cms/plugin-ai-moderation@1176

@emdash-cms/plugin-atproto

npm i https://pkg.pr.new/@emdash-cms/plugin-atproto@1176

@emdash-cms/plugin-audit-log

npm i https://pkg.pr.new/@emdash-cms/plugin-audit-log@1176

@emdash-cms/plugin-color

npm i https://pkg.pr.new/@emdash-cms/plugin-color@1176

@emdash-cms/plugin-embeds

npm i https://pkg.pr.new/@emdash-cms/plugin-embeds@1176

@emdash-cms/plugin-forms

npm i https://pkg.pr.new/@emdash-cms/plugin-forms@1176

@emdash-cms/plugin-webhook-notifier

npm i https://pkg.pr.new/@emdash-cms/plugin-webhook-notifier@1176

commit: ef0fcb8

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds an inline language picker to TipTap code blocks in both the admin PortableTextEditor and the runtime inline visual editor, persisting the chosen language on the code block node attribute and round-tripping through Portable Text.

Changes:

  • Swap StarterKit’s default codeBlock for custom CodeBlock extensions that render a node view with a language chip + popover input.
  • Introduce curated language lists + normalization helpers (admin + duplicated runtime version) and add unit tests for them.
  • Add required TipTap dependency (@tiptap/extension-code-block) and a changeset entry.

Reviewed changes

Copilot reviewed 11 out of 12 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
pnpm-workspace.yaml Adds @tiptap/extension-code-block to the workspace catalog.
pnpm-lock.yaml Locks the new TipTap extension version and updates lockfile resolution.
packages/core/package.json Adds runtime dependency on @tiptap/extension-code-block.
packages/core/src/components/InlinePortableTextEditor.tsx Replaces StarterKit codeBlock with InlineCodeBlockExtension.
packages/core/src/components/inline-code-block.tsx New runtime code block node view with inline language picker + normalization.
packages/admin/package.json Adds admin dependency on @tiptap/extension-code-block.
packages/admin/src/components/PortableTextEditor.tsx Replaces StarterKit codeBlock with CodeBlockExtension.
packages/admin/src/components/editor/CodeBlockNode.tsx New admin code block node view (Kumo/Lingui) with inline language picker.
packages/admin/src/components/editor/codeBlockLanguages.ts New curated language list + findLanguage/normalizeLanguage/languageLabel helpers.
packages/admin/tests/editor/CodeBlockNode.test.ts New tests validating the custom code block extension behavior.
packages/admin/tests/editor/codeBlockLanguages.test.ts New tests for alias resolution and normalization helpers.
.changeset/code-block-language-picker.md Declares minor version bumps for the admin + root package.
Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported

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

Comment on lines +81 to +88
export function normalizeLanguage(value: string | null | undefined): string | undefined {
if (!value) return undefined;
const trimmed = value.trim();
if (!trimmed) return undefined;
const match = findLanguage(trimmed);
if (match) return match.id;
return trimmed.toLowerCase();
}
Comment on lines +26 to +35
const DATALIST_ID = "emdash-code-block-languages";

function CodeBlockLanguageDatalist() {
return (
<datalist id={DATALIST_ID}>
{CODE_BLOCK_LANGUAGES.map((lang) => (
<option key={lang.id} value={lang.id} label={lang.label} />
))}
</datalist>
);
Comment on lines +74 to +81
function normalizeLanguage(value: string | null | undefined): string | undefined {
if (!value) return undefined;
const trimmed = value.trim();
if (!trimmed) return undefined;
const match = findLanguage(trimmed);
if (match) return match.id;
return trimmed.toLowerCase();
}
Comment on lines +61 to +98
const DATALIST_ID = "emdash-inline-code-block-languages";

function findLanguage(value: string | null | undefined): CodeBlockLanguage | null {
if (!value) return null;
const needle = value.trim().toLowerCase();
if (!needle) return null;
for (const lang of CODE_BLOCK_LANGUAGES) {
if (lang.id === needle) return lang;
if (lang.aliases?.includes(needle)) return lang;
}
return null;
}

function normalizeLanguage(value: string | null | undefined): string | undefined {
if (!value) return undefined;
const trimmed = value.trim();
if (!trimmed) return undefined;
const match = findLanguage(trimmed);
if (match) return match.id;
return trimmed.toLowerCase();
}

function languageLabel(value: string | null | undefined): string {
if (!value) return "Plain text";
const match = findLanguage(value);
if (match) return match.label;
return value;
}

function CodeBlockLanguageDatalist() {
return (
<datalist id={DATALIST_ID}>
{CODE_BLOCK_LANGUAGES.map((lang) => (
<option key={lang.id} value={lang.id} label={lang.label} />
))}
</datalist>
);
}
Comment on lines +219 to +230
<div
contentEditable={false}
style={{
position: "absolute",
top: "0.5rem",
insetInlineEnd: "0.5rem",
userSelect: "none",
zIndex: 1,
opacity: chipVisible ? 1 : 0,
pointerEvents: chipVisible ? "auto" : "none",
transition: "opacity 0.15s",
}}
@ascorbic ascorbic mentioned this pull request May 26, 2026
10 tasks
ascorbic added 2 commits May 26, 2026 16:21
Adds an inline language picker chip to code blocks in both the admin
rich text editor and the inline visual editor. Hover reveals the chip,
clicking opens a popover with a free-text input and a curated suggestions
datalist. Aliases (ts, js, c++, etc.) normalize to canonical ids on
commit. The existing triple-backtick markdown shortcut continues to
pre-populate the language. Storage and frontend rendering are unchanged:
the language attribute round-trips through Portable Text and emits as a
language-{id} class on the rendered pre/code.
- Migrate the admin code-block language picker from a native <datalist>
  to Kumo's Autocomplete (landed in Kumo 2.0). Filters by label, id, or
  alias; renders a proper popup list rather than relying on the browser's
  native datalist UI.
- Sanitize unknown language inputs to a single safe CSS class token in
  normalizeLanguage. Previously a user could type "Objective C" and the
  frontend would render class="language-objective c" (two classes).
  Now collapses runs of disallowed chars to a single hyphen and trims
  leading/trailing hyphens. Added unit tests for whitespace, dots,
  slashes, punctuation-only inputs, and other edge cases.
- Use React.useId() for the inline editor's <datalist> id so multiple
  code blocks (or multiple inline editors) on the same page don't
  produce duplicate DOM ids.
- When the language chip is hidden, set tabIndex={-1} and aria-hidden
  so keyboard users don't land on an invisible focus target.

Addresses review feedback on #1176.
@ascorbic ascorbic force-pushed the code-blocks-language-picker branch from bd68fef to ef0fcb8 Compare May 26, 2026 15:41
@ascorbic
Copy link
Copy Markdown
Collaborator Author

Addressed review feedback + migrated to Kumo Autocomplete

Rebased on main (now includes the Kumo 2 bump from #1177).

Review comments addressed

  • Whitespace/punctuation in unknown language ids (normalizeLanguage in admin + inline). Now collapses runs of non-[a-z0-9_-] characters into a single - and trims leading/trailing hyphens so the value is always a single safe CSS class token. "Objective C""objective-c", "foo.bar""foo-bar", "@swift""swift", "!!!"undefined. 5 new unit tests cover the edge cases.

  • Duplicate <datalist id> per code block (inline editor). Now uses React.useId() so each node view gets a unique id, both on the <datalist> and on the input's list attribute. The admin editor no longer renders a datalist at all (see Autocomplete migration below).

  • Hidden chip still tab-focusable (inline editor). When the chip is hidden, sets tabIndex={-1} on the button and aria-hidden on its container so keyboard users don't land on an invisible target. Same treatment now applies in the admin editor.

Bonus: Kumo Autocomplete

With Kumo 2 in (#1177), the admin editor now uses <Autocomplete> instead of a native <datalist>. Filters by label, canonical id, or alias (typing ts finds TypeScript; typing type finds TypeScript; typing c++ finds C++). The popup renders inline in the editor rather than relying on the browser's native datalist UI, so it's discoverable in headless test environments and looks consistent with the rest of the admin. The inline visual editor stays on <datalist> because packages/core can't depend on Kumo.

All 913 admin tests pass, 0 lint diagnostics, typecheck clean.

@ascorbic ascorbic merged commit fae97ee into main May 26, 2026
34 of 35 checks passed
@ascorbic ascorbic deleted the code-blocks-language-picker branch May 26, 2026 16:38
@emdashbot emdashbot Bot mentioned this pull request May 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants