@lexical/code-shiki 0.44.1-nightly.20260519.0 → 0.45.1-dev.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.
@@ -8,7 +8,14 @@
8
8
  import type { LexicalEditor, LexicalNode } from 'lexical';
9
9
  import { CodeNode } from '@lexical/code-core';
10
10
  export interface Tokenizer {
11
- defaultLanguage: string;
11
+ /**
12
+ * Language to fall back to when a {@link CodeNode} doesn't carry one.
13
+ * Set to `null` to opt out of the implicit fallback — code blocks
14
+ * without a language stay untouched (no `data-language` attribute, no
15
+ * syntax highlighting) so a markdown round-trip can preserve ``` with
16
+ * no info string.
17
+ */
18
+ defaultLanguage: string | null;
12
19
  defaultTheme: string;
13
20
  $tokenize: (this: Tokenizer, codeNode: CodeNode, language?: string) => LexicalNode[];
14
21
  }
@@ -153,14 +153,12 @@ function mapTokensToLexicalStructure(tokens, diff) {
153
153
  text = text.slice(1);
154
154
  }
155
155
  }
156
- const parts = text.split('\t');
157
- parts.forEach((part, pidx) => {
158
- if (pidx) {
159
- nodes.push(lexical.$createTabNode());
160
- }
161
- if (part !== '') {
156
+ const style = core.stringifyTokenStyle(token.htmlStyle || core.getTokenStyleObject(token));
157
+ lexical.tokenizeRawText(text, {
158
+ linebreak: () => nodes.push(lexical.$createLineBreakNode()),
159
+ tab: () => nodes.push(lexical.$createTabNode()),
160
+ text: part => {
162
161
  const node = codeCore.$createCodeHighlightNode(part);
163
- const style = core.stringifyTokenStyle(token.htmlStyle || core.getTokenStyleObject(token));
164
162
  node.setStyle(style);
165
163
  nodes.push(node);
166
164
  }
@@ -181,7 +179,8 @@ function mapTokensToLexicalStructure(tokens, diff) {
181
179
  const DEFAULT_CODE_THEME = 'one-light';
182
180
  const ShikiTokenizer = {
183
181
  $tokenize(codeNode, language) {
184
- return $getHighlightNodes(codeNode, language || this.defaultLanguage);
182
+ const lang = language || this.defaultLanguage;
183
+ return lang === null ? codeCore.$plainifyCodeContent(codeNode.getTextContent()) : $getHighlightNodes(codeNode, lang);
185
184
  },
186
185
  defaultLanguage: codeCore.DEFAULT_CODE_LANGUAGE,
187
186
  defaultTheme: DEFAULT_CODE_THEME
@@ -227,9 +226,12 @@ function $codeNodeTransform(editor, tokenizer, transformState, node) {
227
226
  nodesCurrentlyHighlighting
228
227
  } = transformState;
229
228
 
230
- // When new code block inserted it might not have language selected
229
+ // When new code block inserted it might not have language selected.
230
+ // Tokenizers configured with `defaultLanguage: null` opt out of the
231
+ // implicit fallback — leave the node unset and skip highlighting so
232
+ // markdown round-trips ``` (no info string) without injecting one.
231
233
  let language = node.getLanguage();
232
- if (!language) {
234
+ if (!language && tokenizer.defaultLanguage !== null) {
233
235
  language = tokenizer.defaultLanguage;
234
236
  node.setLanguage(language);
235
237
  }
@@ -243,20 +245,31 @@ function $codeNodeTransform(editor, tokenizer, transformState, node) {
243
245
  let inFlight = false;
244
246
  if (!isCodeThemeLoaded(theme)) {
245
247
  loadCodeTheme(theme, editor, nodeKey);
246
- inFlight = true;
248
+ // Only the highlight path (a resolved language) consumes the theme. With
249
+ // no language the text is plainified, which needs no theme, so don't defer
250
+ // the split on a theme load that won't be used — otherwise a code block
251
+ // with `defaultLanguage: null` stays an unsplit TextNode until the theme
252
+ // happens to finish loading.
253
+ if (language) {
254
+ inFlight = true;
255
+ }
247
256
  }
248
257
 
249
258
  // dynamic import of languages
250
- if (isCodeLanguageLoaded(language)) {
251
- if (!node.getIsSyntaxHighlightSupported()) {
252
- node.setIsSyntaxHighlightSupported(true);
253
- }
254
- } else {
255
- if (node.getIsSyntaxHighlightSupported()) {
256
- node.setIsSyntaxHighlightSupported(false);
259
+ if (language) {
260
+ if (isCodeLanguageLoaded(language)) {
261
+ if (!node.getIsSyntaxHighlightSupported()) {
262
+ node.setIsSyntaxHighlightSupported(true);
263
+ }
264
+ } else {
265
+ if (node.getIsSyntaxHighlightSupported()) {
266
+ node.setIsSyntaxHighlightSupported(false);
267
+ }
268
+ loadCodeLanguage(language, editor, nodeKey);
269
+ inFlight = true;
257
270
  }
258
- loadCodeLanguage(language, editor, nodeKey);
259
- inFlight = true;
271
+ } else if (node.getIsSyntaxHighlightSupported()) {
272
+ node.setIsSyntaxHighlightSupported(false);
260
273
  }
261
274
  if (inFlight) {
262
275
  return;
@@ -278,7 +291,7 @@ function $codeNodeTransform(editor, tokenizer, transformState, node) {
278
291
  return false;
279
292
  }
280
293
  const lang = currentNode.getLanguage() || tokenizer.defaultLanguage;
281
- const highlightNodes = tokenizer.$tokenize(currentNode, lang);
294
+ const highlightNodes = tokenizer.$tokenize(currentNode, lang ?? undefined);
282
295
  const diffRange = getDiffRange(currentNode.getChildren(), highlightNodes);
283
296
  const {
284
297
  from,
@@ -6,9 +6,9 @@
6
6
  *
7
7
  */
8
8
 
9
- import { $isCodeNode, $createCodeHighlightNode, CodeExtension, CodeIndentExtension, DEFAULT_CODE_LANGUAGE, CodeNode, CodeHighlightNode, $isCodeHighlightNode, registerCodeIndentation } from '@lexical/code-core';
9
+ import { $isCodeNode, $createCodeHighlightNode, CodeExtension, CodeIndentExtension, DEFAULT_CODE_LANGUAGE, CodeNode, CodeHighlightNode, $plainifyCodeContent, $isCodeHighlightNode, registerCodeIndentation } from '@lexical/code-core';
10
10
  import { effect, namedSignals } from '@lexical/extension';
11
- import { $getNodeByKey, $createLineBreakNode, $createTabNode, defineExtension, safeCast, TextNode, mergeRegister, $isLineBreakNode, $onUpdate, $createTextNode, $getSelection, $isRangeSelection, $isTextNode, $isTabNode } from 'lexical';
11
+ import { $getNodeByKey, $createLineBreakNode, tokenizeRawText, $createTabNode, defineExtension, safeCast, TextNode, mergeRegister, $isLineBreakNode, $onUpdate, $createTextNode, $getSelection, $isRangeSelection, $isTextNode, $isTabNode } from 'lexical';
12
12
  import { createHighlighterCoreSync, isSpecialTheme, isSpecialLang, stringifyTokenStyle, getTokenStyleObject } from '@shikijs/core';
13
13
  import { createJavaScriptRegexEngine } from '@shikijs/engine-javascript';
14
14
  import { bundledLanguagesInfo } from 'shiki/langs';
@@ -151,14 +151,12 @@ function mapTokensToLexicalStructure(tokens, diff) {
151
151
  text = text.slice(1);
152
152
  }
153
153
  }
154
- const parts = text.split('\t');
155
- parts.forEach((part, pidx) => {
156
- if (pidx) {
157
- nodes.push($createTabNode());
158
- }
159
- if (part !== '') {
154
+ const style = stringifyTokenStyle(token.htmlStyle || getTokenStyleObject(token));
155
+ tokenizeRawText(text, {
156
+ linebreak: () => nodes.push($createLineBreakNode()),
157
+ tab: () => nodes.push($createTabNode()),
158
+ text: part => {
160
159
  const node = $createCodeHighlightNode(part);
161
- const style = stringifyTokenStyle(token.htmlStyle || getTokenStyleObject(token));
162
160
  node.setStyle(style);
163
161
  nodes.push(node);
164
162
  }
@@ -179,7 +177,8 @@ function mapTokensToLexicalStructure(tokens, diff) {
179
177
  const DEFAULT_CODE_THEME = 'one-light';
180
178
  const ShikiTokenizer = {
181
179
  $tokenize(codeNode, language) {
182
- return $getHighlightNodes(codeNode, language || this.defaultLanguage);
180
+ const lang = language || this.defaultLanguage;
181
+ return lang === null ? $plainifyCodeContent(codeNode.getTextContent()) : $getHighlightNodes(codeNode, lang);
183
182
  },
184
183
  defaultLanguage: DEFAULT_CODE_LANGUAGE,
185
184
  defaultTheme: DEFAULT_CODE_THEME
@@ -225,9 +224,12 @@ function $codeNodeTransform(editor, tokenizer, transformState, node) {
225
224
  nodesCurrentlyHighlighting
226
225
  } = transformState;
227
226
 
228
- // When new code block inserted it might not have language selected
227
+ // When new code block inserted it might not have language selected.
228
+ // Tokenizers configured with `defaultLanguage: null` opt out of the
229
+ // implicit fallback — leave the node unset and skip highlighting so
230
+ // markdown round-trips ``` (no info string) without injecting one.
229
231
  let language = node.getLanguage();
230
- if (!language) {
232
+ if (!language && tokenizer.defaultLanguage !== null) {
231
233
  language = tokenizer.defaultLanguage;
232
234
  node.setLanguage(language);
233
235
  }
@@ -241,20 +243,31 @@ function $codeNodeTransform(editor, tokenizer, transformState, node) {
241
243
  let inFlight = false;
242
244
  if (!isCodeThemeLoaded(theme)) {
243
245
  loadCodeTheme(theme, editor, nodeKey);
244
- inFlight = true;
246
+ // Only the highlight path (a resolved language) consumes the theme. With
247
+ // no language the text is plainified, which needs no theme, so don't defer
248
+ // the split on a theme load that won't be used — otherwise a code block
249
+ // with `defaultLanguage: null` stays an unsplit TextNode until the theme
250
+ // happens to finish loading.
251
+ if (language) {
252
+ inFlight = true;
253
+ }
245
254
  }
246
255
 
247
256
  // dynamic import of languages
248
- if (isCodeLanguageLoaded(language)) {
249
- if (!node.getIsSyntaxHighlightSupported()) {
250
- node.setIsSyntaxHighlightSupported(true);
251
- }
252
- } else {
253
- if (node.getIsSyntaxHighlightSupported()) {
254
- node.setIsSyntaxHighlightSupported(false);
257
+ if (language) {
258
+ if (isCodeLanguageLoaded(language)) {
259
+ if (!node.getIsSyntaxHighlightSupported()) {
260
+ node.setIsSyntaxHighlightSupported(true);
261
+ }
262
+ } else {
263
+ if (node.getIsSyntaxHighlightSupported()) {
264
+ node.setIsSyntaxHighlightSupported(false);
265
+ }
266
+ loadCodeLanguage(language, editor, nodeKey);
267
+ inFlight = true;
255
268
  }
256
- loadCodeLanguage(language, editor, nodeKey);
257
- inFlight = true;
269
+ } else if (node.getIsSyntaxHighlightSupported()) {
270
+ node.setIsSyntaxHighlightSupported(false);
258
271
  }
259
272
  if (inFlight) {
260
273
  return;
@@ -276,7 +289,7 @@ function $codeNodeTransform(editor, tokenizer, transformState, node) {
276
289
  return false;
277
290
  }
278
291
  const lang = currentNode.getLanguage() || tokenizer.defaultLanguage;
279
- const highlightNodes = tokenizer.$tokenize(currentNode, lang);
292
+ const highlightNodes = tokenizer.$tokenize(currentNode, lang ?? undefined);
280
293
  const diffRange = getDiffRange(currentNode.getChildren(), highlightNodes);
281
294
  const {
282
295
  from,
@@ -19,7 +19,7 @@ import type {CodeNode} from '@lexical/code';
19
19
  */
20
20
 
21
21
  export type Tokenizer = {
22
- defaultLanguage: string;
22
+ defaultLanguage: string | null;
23
23
  defaultTheme: string;
24
24
  $tokenize: (codeNode: CodeNode, language?: string) => LexicalNode[];
25
25
  }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ */
8
+
9
+ "use strict";var e=require("@lexical/code-core"),t=require("@lexical/extension"),n=require("lexical"),i=require("@shikijs/core"),o=require("@shikijs/engine-javascript"),r=require("shiki/langs"),d=require("shiki/themes");const s=i.createHighlighterCoreSync({engine:o.createJavaScriptRegexEngine(),langs:[],themes:[]});function g(e){const t=/^diff-([\w-]+)/i.exec(e);return t?t[1]:null}function a(e){const t=g(e)||e;return!!i.isSpecialLang(t)||s.getLoadedLanguages().includes(t)}function l(t,i,o){const d=g(t),l=d||t;if(!a(l)){const d=r.bundledLanguagesInfo.find(e=>e.id===l||e.aliases&&e.aliases.includes(l));if(d)return s.loadLanguage(d.import()).then(()=>{i&&o&&i.update(()=>{const i=n.$getNodeByKey(o);e.$isCodeNode(i)&&i.getLanguage()===t&&!i.getIsSyntaxHighlightSupported()&&i.setIsSyntaxHighlightSupported(!0)})})}}function u(e){const t=e;return!!i.isSpecialTheme(t)||s.getLoadedThemes().includes(t)}function c(t,i,o){if(!u(t)){const r=d.bundledThemesInfo.find(e=>e.id===t);if(r)return s.loadTheme(r.import()).then(()=>{i&&o&&i.update(()=>{const t=n.$getNodeByKey(o);e.$isCodeNode(t)&&t.markDirty()})})}}function h(t,o){const r=/^diff-([\w-]+)/i.exec(o),d=t.getTextContent(),g=s.codeToTokens(d,{lang:r?r[1]:o,theme:t.getTheme()||"poimandres"}),{tokens:a,bg:l,fg:u}=g;let c="";return l&&(c+=`background-color: ${l};`),u&&(c+=`color: ${u};`),t.getStyle()!==c&&t.setStyle(c),function(t,o){const r=[];return t.forEach((t,d)=>{d&&r.push(n.$createLineBreakNode()),t.forEach((t,d)=>{let s=t.content;if(o&&0===d&&s.length>0){const t=["+","-",">","<"," "],n=["inserted","deleted","inserted","deleted","unchanged"],i=t.indexOf(s[0]);-1!==i&&(r.push(e.$createCodeHighlightNode(t[i],n[i])),s=s.slice(1))}const g=i.stringifyTokenStyle(t.htmlStyle||i.getTokenStyleObject(t));n.tokenizeRawText(s,{linebreak:()=>r.push(n.$createLineBreakNode()),tab:()=>r.push(n.$createTabNode()),text:t=>{const n=e.$createCodeHighlightNode(t);n.setStyle(g),r.push(n)}})})}),r}(a,!!r)}const f={$tokenize(t,n){const i=n||this.defaultLanguage;return null===i?e.$plainifyCodeContent(t.getTextContent()):h(t,i)},defaultLanguage:e.DEFAULT_CODE_LANGUAGE,defaultTheme:"one-light"};function p(t,i,o,r){const d=r.getParent();e.$isCodeNode(d)?x(t,i,o,d):e.$isCodeHighlightNode(r)&&r.replace(n.$createTextNode(r.__text))}function C(e,t){const i=t.getElementByKey(e.getKey());if(null===i)return;const o=e.getChildren(),r=o.length;if(r===i.__cachedChildrenLength)return;i.__cachedChildrenLength=r;let d="1",s=1;for(let e=0;e<r;e++)n.$isLineBreakNode(o[e])&&(d+="\n"+ ++s);i.setAttribute("data-gutter",d)}function x(t,i,o,r){const d=r.getKey(),{nodesCurrentlyHighlighting:s}=o;let g=r.getLanguage();g||null===i.defaultLanguage||(g=i.defaultLanguage,r.setLanguage(g));let h=r.getTheme();h||(h=i.defaultTheme,r.setTheme(h));let f=!1;u(h)||(c(h,t,d),g&&(f=!0)),g?a(g)?r.getIsSyntaxHighlightSupported()||r.setIsSyntaxHighlightSupported(!0):(r.getIsSyntaxHighlightSupported()&&r.setIsSyntaxHighlightSupported(!1),l(g,t,d),f=!0):r.getIsSyntaxHighlightSupported()&&r.setIsSyntaxHighlightSupported(!1),f||s.has(d)||(s.add(d),o.didTransform||(o.didTransform=!0,n.$onUpdate(()=>{o.didTransform=!1,s.clear()})),function(t,i){const o=n.$getNodeByKey(t);if(!e.$isCodeNode(o)||!o.isAttached())return;const r=n.$getSelection();if(!n.$isRangeSelection(r))return void i();const d=r.anchor,s=d.offset,g="element"===d.type&&n.$isLineBreakNode(o.getChildAtIndex(d.offset-1));let a=0;if(!g){const e=d.getNode();a=s+e.getPreviousSiblings().reduce((e,t)=>e+t.getTextContentSize(),0)}if(!i())return;if(g)return void d.getNode().select(s,s);o.getChildren().some(e=>{const t=n.$isTextNode(e);if(t||n.$isLineBreakNode(e)){const n=e.getTextContentSize();if(t&&n>=a)return e.select(a,a),!0;a-=n}return!1})}(d,()=>{const t=n.$getNodeByKey(d);if(!e.$isCodeNode(t)||!t.isAttached())return!1;const o=t.getLanguage()||i.defaultLanguage,s=i.$tokenize(t,o??void 0),g=function(e,t){let n=0;for(;n<e.length&&m(e[n],t[n]);)n++;const i=e.length,o=t.length,r=Math.min(i,o)-n;let d=0;for(;d<r;)if(d++,!m(e[i-d],t[o-d])){d--;break}const s=n,g=i-d,a=t.slice(n,o-d);return{from:s,nodesForReplacement:a,to:g}}(t.getChildren(),s),{from:a,to:l,nodesForReplacement:u}=g;return!(a===l&&!u.length)&&(r.splice(a,l-a,u),!0)}))}function m(t,i){return e.$isCodeHighlightNode(t)&&e.$isCodeHighlightNode(i)&&t.__text===i.__text&&t.__highlightType===i.__highlightType&&t.__style===i.__style||n.$isTabNode(t)&&n.$isTabNode(i)||n.$isLineBreakNode(t)&&n.$isLineBreakNode(i)}function y(t,i){const o=[];!0!==t._headless&&o.push(t.registerMutationListener(e.CodeNode,e=>{t.getEditorState().read(()=>{for(const[i,o]of e)if("destroyed"!==o){const e=n.$getNodeByKey(i);null!==e&&C(e,t)}})},{skipInitialization:!1}));const r={didTransform:!1,nodesCurrentlyHighlighting:new Set};return o.push(t.registerNodeTransform(e.CodeNode,x.bind(null,t,i,r)),t.registerNodeTransform(n.TextNode,p.bind(null,t,i,r)),t.registerNodeTransform(e.CodeHighlightNode,p.bind(null,t,i,r))),n.mergeRegister(...o)}const N=n.defineExtension({build:(e,n)=>t.namedSignals(n),config:n.safeCast({disabled:!1,tokenizer:f}),dependencies:[e.CodeExtension,e.CodeIndentExtension],name:"@lexical/code-shiki",register:(e,n,i)=>{const o=i.getOutput();return t.effect(()=>{if(!o.disabled.value)return y(e,o.tokenizer.value)})}}),T=n.defineExtension({config:n.safeCast(f),dependencies:[N],init:(e,t,n)=>{n.getDependency(N).config.tokenizer=t},name:"@lexical/code-shiki/legacy"});exports.CodeHighlighterShikiExtension=T,exports.CodeShikiExtension=N,exports.ShikiTokenizer=f,exports.getCodeLanguageOptions=function(){return r.bundledLanguagesInfo.map(e=>[e.id,e.name])},exports.getCodeThemeOptions=function(){return d.bundledThemesInfo.map(e=>[e.id,e.displayName])},exports.isCodeLanguageLoaded=a,exports.loadCodeLanguage=l,exports.loadCodeTheme=c,exports.normalizeCodeLanguage=function(e){const t=e,n=r.bundledLanguagesInfo.find(e=>e.id===t||e.aliases&&e.aliases.includes(t));return n?n.id:e},exports.registerCodeHighlighting=function(t,i=f){if(!t.hasNodes([e.CodeNode,e.CodeHighlightNode]))throw new Error("CodeHighlightPlugin: CodeNode or CodeHighlightNode not registered on editor");return n.mergeRegister(y(t,i),e.registerCodeIndentation(t))};
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ */
8
+
9
+ import{$isCodeNode as e,$createCodeHighlightNode as t,CodeExtension as n,CodeIndentExtension as i,DEFAULT_CODE_LANGUAGE as o,CodeNode as r,CodeHighlightNode as s,$plainifyCodeContent as l,$isCodeHighlightNode as d,registerCodeIndentation as g}from"@lexical/code-core";import{effect as a,namedSignals as u}from"@lexical/extension";import{$getNodeByKey as c,$createLineBreakNode as h,tokenizeRawText as f,$createTabNode as p,defineExtension as m,safeCast as y,TextNode as x,mergeRegister as S,$isLineBreakNode as T,$onUpdate as _,$createTextNode as k,$getSelection as L,$isRangeSelection as C,$isTextNode as b,$isTabNode as H}from"lexical";import{createHighlighterCoreSync as I,isSpecialTheme as N,isSpecialLang as z,stringifyTokenStyle as v,getTokenStyleObject as w}from"@shikijs/core";import{createJavaScriptRegexEngine as E}from"@shikijs/engine-javascript";import{bundledLanguagesInfo as A}from"shiki/langs";import{bundledThemesInfo as $}from"shiki/themes";const j=I({engine:E(),langs:[],themes:[]});function K(e){const t=/^diff-([\w-]+)/i.exec(e);return t?t[1]:null}function P(e){const t=K(e)||e;return!!z(t)||j.getLoadedLanguages().includes(t)}function D(t,n,i){const o=K(t),r=o||t;if(!P(r)){const o=A.find(e=>e.id===r||e.aliases&&e.aliases.includes(r));if(o)return j.loadLanguage(o.import()).then(()=>{n&&i&&n.update(()=>{const n=c(i);e(n)&&n.getLanguage()===t&&!n.getIsSyntaxHighlightSupported()&&n.setIsSyntaxHighlightSupported(!0)})})}}function F(e){const t=e;return!!N(t)||j.getLoadedThemes().includes(t)}function M(t,n,i){if(!F(t)){const o=$.find(e=>e.id===t);if(o)return j.loadTheme(o.import()).then(()=>{n&&i&&n.update(()=>{const t=c(i);e(t)&&t.markDirty()})})}}function O(){return A.map(e=>[e.id,e.name])}function R(){return $.map(e=>[e.id,e.displayName])}function B(e){const t=e,n=A.find(e=>e.id===t||e.aliases&&e.aliases.includes(t));return n?n.id:e}function q(e,n){const i=/^diff-([\w-]+)/i.exec(n),o=e.getTextContent(),r=j.codeToTokens(o,{lang:i?i[1]:n,theme:e.getTheme()||"poimandres"}),{tokens:s,bg:l,fg:d}=r;let g="";return l&&(g+=`background-color: ${l};`),d&&(g+=`color: ${d};`),e.getStyle()!==g&&e.setStyle(g),function(e,n){const i=[];return e.forEach((e,o)=>{o&&i.push(h()),e.forEach((e,o)=>{let r=e.content;if(n&&0===o&&r.length>0){const e=["+","-",">","<"," "],n=["inserted","deleted","inserted","deleted","unchanged"],o=e.indexOf(r[0]);-1!==o&&(i.push(t(e[o],n[o])),r=r.slice(1))}const s=v(e.htmlStyle||w(e));f(r,{linebreak:()=>i.push(h()),tab:()=>i.push(p()),text:e=>{const n=t(e);n.setStyle(s),i.push(n)}})})}),i}(s,!!i)}const G={$tokenize(e,t){const n=t||this.defaultLanguage;return null===n?l(e.getTextContent()):q(e,n)},defaultLanguage:o,defaultTheme:"one-light"};function J(t,n,i,o){const r=o.getParent();e(r)?U(t,n,i,r):d(o)&&o.replace(k(o.__text))}function Q(e,t){const n=t.getElementByKey(e.getKey());if(null===n)return;const i=e.getChildren(),o=i.length;if(o===n.__cachedChildrenLength)return;n.__cachedChildrenLength=o;let r="1",s=1;for(let e=0;e<o;e++)T(i[e])&&(r+="\n"+ ++s);n.setAttribute("data-gutter",r)}function U(t,n,i,o){const r=o.getKey(),{nodesCurrentlyHighlighting:s}=i;let l=o.getLanguage();l||null===n.defaultLanguage||(l=n.defaultLanguage,o.setLanguage(l));let d=o.getTheme();d||(d=n.defaultTheme,o.setTheme(d));let g=!1;F(d)||(M(d,t,r),l&&(g=!0)),l?P(l)?o.getIsSyntaxHighlightSupported()||o.setIsSyntaxHighlightSupported(!0):(o.getIsSyntaxHighlightSupported()&&o.setIsSyntaxHighlightSupported(!1),D(l,t,r),g=!0):o.getIsSyntaxHighlightSupported()&&o.setIsSyntaxHighlightSupported(!1),g||s.has(r)||(s.add(r),i.didTransform||(i.didTransform=!0,_(()=>{i.didTransform=!1,s.clear()})),function(t,n){const i=c(t);if(!e(i)||!i.isAttached())return;const o=L();if(!C(o))return void n();const r=o.anchor,s=r.offset,l="element"===r.type&&T(i.getChildAtIndex(r.offset-1));let d=0;if(!l){const e=r.getNode();d=s+e.getPreviousSiblings().reduce((e,t)=>e+t.getTextContentSize(),0)}if(!n())return;if(l)return void r.getNode().select(s,s);i.getChildren().some(e=>{const t=b(e);if(t||T(e)){const n=e.getTextContentSize();if(t&&n>=d)return e.select(d,d),!0;d-=n}return!1})}(r,()=>{const t=c(r);if(!e(t)||!t.isAttached())return!1;const i=t.getLanguage()||n.defaultLanguage,s=n.$tokenize(t,i??void 0),l=function(e,t){let n=0;for(;n<e.length&&V(e[n],t[n]);)n++;const i=e.length,o=t.length,r=Math.min(i,o)-n;let s=0;for(;s<r;)if(s++,!V(e[i-s],t[o-s])){s--;break}const l=n,d=i-s,g=t.slice(n,o-s);return{from:l,nodesForReplacement:g,to:d}}(t.getChildren(),s),{from:d,to:g,nodesForReplacement:a}=l;return!(d===g&&!a.length)&&(o.splice(d,g-d,a),!0)}))}function V(e,t){return d(e)&&d(t)&&e.__text===t.__text&&e.__highlightType===t.__highlightType&&e.__style===t.__style||H(e)&&H(t)||T(e)&&T(t)}function W(e,t){const n=[];!0!==e._headless&&n.push(e.registerMutationListener(r,t=>{e.getEditorState().read(()=>{for(const[n,i]of t)if("destroyed"!==i){const t=c(n);null!==t&&Q(t,e)}})},{skipInitialization:!1}));const i={didTransform:!1,nodesCurrentlyHighlighting:new Set};return n.push(e.registerNodeTransform(r,U.bind(null,e,t,i)),e.registerNodeTransform(x,J.bind(null,e,t,i)),e.registerNodeTransform(s,J.bind(null,e,t,i))),S(...n)}function X(e,t=G){if(!e.hasNodes([r,s]))throw new Error("CodeHighlightPlugin: CodeNode or CodeHighlightNode not registered on editor");return S(W(e,t),g(e))}const Y=m({build:(e,t)=>u(t),config:y({disabled:!1,tokenizer:G}),dependencies:[n,i],name:"@lexical/code-shiki",register:(e,t,n)=>{const i=n.getOutput();return a(()=>{if(!i.disabled.value)return W(e,i.tokenizer.value)})}}),Z=m({config:y(G),dependencies:[Y],init:(e,t,n)=>{n.getDependency(Y).config.tokenizer=t},name:"@lexical/code-shiki/legacy"});export{Z as CodeHighlighterShikiExtension,Y as CodeShikiExtension,G as ShikiTokenizer,O as getCodeLanguageOptions,R as getCodeThemeOptions,P as isCodeLanguageLoaded,D as loadCodeLanguage,M as loadCodeTheme,B as normalizeCodeLanguage,X as registerCodeHighlighting};
package/package.json CHANGED
@@ -8,18 +8,18 @@
8
8
  "code"
9
9
  ],
10
10
  "license": "MIT",
11
- "version": "0.44.1-nightly.20260519.0",
12
- "main": "LexicalCodeShiki.js",
13
- "types": "index.d.ts",
11
+ "version": "0.45.1-dev.0",
12
+ "main": "./dist/LexicalCodeShiki.js",
13
+ "types": "./dist/index.d.ts",
14
14
  "dependencies": {
15
- "@lexical/code-core": "0.44.1-nightly.20260519.0",
16
- "@lexical/extension": "0.44.1-nightly.20260519.0",
17
15
  "@shikijs/core": "^4.0.2",
18
16
  "@shikijs/engine-javascript": "^4.0.2",
19
17
  "@shikijs/langs": "^4.0.2",
20
18
  "@shikijs/themes": "^4.0.2",
21
- "lexical": "0.44.1-nightly.20260519.0",
22
- "shiki": "^4.0.2"
19
+ "shiki": "^4.0.2",
20
+ "@lexical/extension": "0.45.1-dev.0",
21
+ "@lexical/code-core": "0.45.1-dev.0",
22
+ "lexical": "0.45.1-dev.0"
23
23
  },
24
24
  "repository": {
25
25
  "type": "git",
@@ -29,23 +29,37 @@
29
29
  "devDependencies": {
30
30
  "@shikijs/types": "^4.0.2"
31
31
  },
32
- "module": "LexicalCodeShiki.mjs",
32
+ "module": "./dist/LexicalCodeShiki.mjs",
33
33
  "sideEffects": true,
34
34
  "exports": {
35
35
  ".": {
36
+ "source": "./src/index.ts",
36
37
  "import": {
37
- "types": "./index.d.ts",
38
- "development": "./LexicalCodeShiki.dev.mjs",
39
- "production": "./LexicalCodeShiki.prod.mjs",
40
- "node": "./LexicalCodeShiki.node.mjs",
41
- "default": "./LexicalCodeShiki.mjs"
38
+ "types": "./dist/index.d.ts",
39
+ "development": "./dist/LexicalCodeShiki.dev.mjs",
40
+ "production": "./dist/LexicalCodeShiki.prod.mjs",
41
+ "node": "./dist/LexicalCodeShiki.node.mjs",
42
+ "default": "./dist/LexicalCodeShiki.mjs"
42
43
  },
43
44
  "require": {
44
- "types": "./index.d.ts",
45
- "development": "./LexicalCodeShiki.dev.js",
46
- "production": "./LexicalCodeShiki.prod.js",
47
- "default": "./LexicalCodeShiki.js"
45
+ "types": "./dist/index.d.ts",
46
+ "development": "./dist/LexicalCodeShiki.dev.js",
47
+ "production": "./dist/LexicalCodeShiki.prod.js",
48
+ "default": "./dist/LexicalCodeShiki.js"
48
49
  }
49
50
  }
50
- }
51
+ },
52
+ "files": [
53
+ "dist",
54
+ "src",
55
+ "!src/__tests__",
56
+ "!src/__bench__",
57
+ "!src/__mocks__",
58
+ "!src/**/*.test.ts",
59
+ "!src/**/*.test.tsx",
60
+ "!src/**/*.bench.ts",
61
+ "!src/**/*.bench.tsx",
62
+ "README.md",
63
+ "LICENSE"
64
+ ]
51
65
  }
@@ -0,0 +1,506 @@
1
+ /**
2
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ */
8
+
9
+ import type {LexicalEditor, LexicalNode, NodeKey} from 'lexical';
10
+
11
+ import {
12
+ $isCodeHighlightNode,
13
+ $isCodeNode,
14
+ $plainifyCodeContent,
15
+ CodeExtension,
16
+ CodeHighlightNode,
17
+ CodeIndentExtension,
18
+ CodeNode,
19
+ DEFAULT_CODE_LANGUAGE,
20
+ registerCodeIndentation,
21
+ } from '@lexical/code-core';
22
+ import {effect, namedSignals} from '@lexical/extension';
23
+ import {
24
+ $createTextNode,
25
+ $getNodeByKey,
26
+ $getSelection,
27
+ $isLineBreakNode,
28
+ $isRangeSelection,
29
+ $isTabNode,
30
+ $isTextNode,
31
+ $onUpdate,
32
+ defineExtension,
33
+ mergeRegister,
34
+ safeCast,
35
+ TextNode,
36
+ } from 'lexical';
37
+
38
+ import {
39
+ $getHighlightNodes,
40
+ isCodeLanguageLoaded,
41
+ isCodeThemeLoaded,
42
+ loadCodeLanguage,
43
+ loadCodeTheme,
44
+ } from './FacadeShiki';
45
+
46
+ export interface Tokenizer {
47
+ /**
48
+ * Language to fall back to when a {@link CodeNode} doesn't carry one.
49
+ * Set to `null` to opt out of the implicit fallback — code blocks
50
+ * without a language stay untouched (no `data-language` attribute, no
51
+ * syntax highlighting) so a markdown round-trip can preserve ``` with
52
+ * no info string.
53
+ */
54
+ defaultLanguage: string | null;
55
+ defaultTheme: string;
56
+ $tokenize: (
57
+ this: Tokenizer,
58
+ codeNode: CodeNode,
59
+ language?: string,
60
+ ) => LexicalNode[];
61
+ }
62
+
63
+ const DEFAULT_CODE_THEME = 'one-light';
64
+
65
+ export const ShikiTokenizer: Tokenizer = {
66
+ $tokenize(
67
+ this: Tokenizer,
68
+ codeNode: CodeNode,
69
+ language?: string,
70
+ ): LexicalNode[] {
71
+ const lang = language || this.defaultLanguage;
72
+ return lang === null
73
+ ? $plainifyCodeContent(codeNode.getTextContent())
74
+ : $getHighlightNodes(codeNode, lang);
75
+ },
76
+ defaultLanguage: DEFAULT_CODE_LANGUAGE,
77
+ defaultTheme: DEFAULT_CODE_THEME,
78
+ };
79
+
80
+ function $textNodeTransform(
81
+ editor: LexicalEditor,
82
+ tokenizer: Tokenizer,
83
+ transformState: TransformState,
84
+ node: TextNode,
85
+ ): void {
86
+ // Since CodeNode has flat children structure we only need to check
87
+ // if node's parent is a code node and run highlighting if so
88
+ const parentNode = node.getParent();
89
+ if ($isCodeNode(parentNode)) {
90
+ $codeNodeTransform(editor, tokenizer, transformState, parentNode);
91
+ } else if ($isCodeHighlightNode(node)) {
92
+ // When code block converted into paragraph or other element
93
+ // code highlight nodes converted back to normal text
94
+ node.replace($createTextNode(node.__text));
95
+ }
96
+ }
97
+
98
+ function updateCodeGutter(node: CodeNode, editor: LexicalEditor): void {
99
+ const codeElement = editor.getElementByKey(node.getKey());
100
+ if (codeElement === null) {
101
+ return;
102
+ }
103
+ const children = node.getChildren();
104
+ const childrenLength = children.length;
105
+ // @ts-ignore: internal field
106
+ if (childrenLength === codeElement.__cachedChildrenLength) {
107
+ // Avoid updating the attribute if the children length hasn't changed.
108
+ return;
109
+ }
110
+ // @ts-ignore:: internal field
111
+ codeElement.__cachedChildrenLength = childrenLength;
112
+ let gutter = '1';
113
+ let count = 1;
114
+ for (let i = 0; i < childrenLength; i++) {
115
+ if ($isLineBreakNode(children[i])) {
116
+ gutter += '\n' + ++count;
117
+ }
118
+ }
119
+ codeElement.setAttribute('data-gutter', gutter);
120
+ }
121
+
122
+ interface TransformState {
123
+ didTransform: boolean;
124
+ // Using extra cache (`nodesCurrentlyHighlighting`) since both CodeNode and CodeHighlightNode
125
+ // transforms might be called at the same time (e.g. new CodeHighlight node inserted) and
126
+ // in both cases we'll rerun whole reformatting over CodeNode, which is redundant.
127
+ // Especially when pasting code into CodeBlock.
128
+ nodesCurrentlyHighlighting: Set<NodeKey>;
129
+ }
130
+
131
+ function $codeNodeTransform(
132
+ editor: LexicalEditor,
133
+ tokenizer: Tokenizer,
134
+ transformState: TransformState,
135
+ node: CodeNode,
136
+ ) {
137
+ const nodeKey = node.getKey();
138
+ const {nodesCurrentlyHighlighting} = transformState;
139
+
140
+ // When new code block inserted it might not have language selected.
141
+ // Tokenizers configured with `defaultLanguage: null` opt out of the
142
+ // implicit fallback — leave the node unset and skip highlighting so
143
+ // markdown round-trips ``` (no info string) without injecting one.
144
+ let language = node.getLanguage();
145
+ if (!language && tokenizer.defaultLanguage !== null) {
146
+ language = tokenizer.defaultLanguage;
147
+ node.setLanguage(language);
148
+ }
149
+
150
+ let theme = node.getTheme();
151
+ if (!theme) {
152
+ theme = tokenizer.defaultTheme;
153
+ node.setTheme(theme);
154
+ }
155
+
156
+ // dynamic import of themes
157
+ let inFlight = false;
158
+ if (!isCodeThemeLoaded(theme)) {
159
+ loadCodeTheme(theme, editor, nodeKey);
160
+ // Only the highlight path (a resolved language) consumes the theme. With
161
+ // no language the text is plainified, which needs no theme, so don't defer
162
+ // the split on a theme load that won't be used — otherwise a code block
163
+ // with `defaultLanguage: null` stays an unsplit TextNode until the theme
164
+ // happens to finish loading.
165
+ if (language) {
166
+ inFlight = true;
167
+ }
168
+ }
169
+
170
+ // dynamic import of languages
171
+ if (language) {
172
+ if (isCodeLanguageLoaded(language)) {
173
+ if (!node.getIsSyntaxHighlightSupported()) {
174
+ node.setIsSyntaxHighlightSupported(true);
175
+ }
176
+ } else {
177
+ if (node.getIsSyntaxHighlightSupported()) {
178
+ node.setIsSyntaxHighlightSupported(false);
179
+ }
180
+ loadCodeLanguage(language, editor, nodeKey);
181
+ inFlight = true;
182
+ }
183
+ } else if (node.getIsSyntaxHighlightSupported()) {
184
+ node.setIsSyntaxHighlightSupported(false);
185
+ }
186
+
187
+ if (inFlight) {
188
+ return;
189
+ }
190
+
191
+ if (nodesCurrentlyHighlighting.has(nodeKey)) {
192
+ return;
193
+ }
194
+
195
+ nodesCurrentlyHighlighting.add(nodeKey);
196
+ if (!transformState.didTransform) {
197
+ transformState.didTransform = true;
198
+ $onUpdate(() => {
199
+ transformState.didTransform = false;
200
+ nodesCurrentlyHighlighting.clear();
201
+ });
202
+ }
203
+
204
+ $updateAndRetainSelection(nodeKey, () => {
205
+ const currentNode = $getNodeByKey(nodeKey);
206
+
207
+ if (!$isCodeNode(currentNode) || !currentNode.isAttached()) {
208
+ return false;
209
+ }
210
+
211
+ const lang = currentNode.getLanguage() || tokenizer.defaultLanguage;
212
+ const highlightNodes = tokenizer.$tokenize(currentNode, lang ?? undefined);
213
+ const diffRange = getDiffRange(currentNode.getChildren(), highlightNodes);
214
+ const {from, to, nodesForReplacement} = diffRange;
215
+
216
+ if (from !== to || nodesForReplacement.length) {
217
+ node.splice(from, to - from, nodesForReplacement);
218
+ return true;
219
+ }
220
+
221
+ return false;
222
+ });
223
+ }
224
+
225
+ // Wrapping update function into selection retainer, that tries to keep cursor at the same
226
+ // position as before.
227
+ function $updateAndRetainSelection(
228
+ nodeKey: NodeKey,
229
+ updateFn: () => boolean,
230
+ ): void {
231
+ const node = $getNodeByKey(nodeKey);
232
+ if (!$isCodeNode(node) || !node.isAttached()) {
233
+ return;
234
+ }
235
+ const selection = $getSelection();
236
+ // If it's not range selection (or null selection) there's no need to change it,
237
+ // but we can still run highlighting logic
238
+ if (!$isRangeSelection(selection)) {
239
+ updateFn();
240
+ return;
241
+ }
242
+
243
+ const anchor = selection.anchor;
244
+ const anchorOffset = anchor.offset;
245
+ const isNewLineAnchor =
246
+ anchor.type === 'element' &&
247
+ $isLineBreakNode(node.getChildAtIndex(anchor.offset - 1));
248
+ let textOffset = 0;
249
+
250
+ // Calculating previous text offset (all text node prior to anchor + anchor own text offset)
251
+ if (!isNewLineAnchor) {
252
+ const anchorNode = anchor.getNode();
253
+ textOffset =
254
+ anchorOffset +
255
+ anchorNode.getPreviousSiblings().reduce((offset, _node) => {
256
+ return offset + _node.getTextContentSize();
257
+ }, 0);
258
+ }
259
+
260
+ const hasChanges = updateFn();
261
+ if (!hasChanges) {
262
+ return;
263
+ }
264
+
265
+ // Non-text anchors only happen for line breaks, otherwise
266
+ // selection will be within text node (code highlight node)
267
+ if (isNewLineAnchor) {
268
+ anchor.getNode().select(anchorOffset, anchorOffset);
269
+ return;
270
+ }
271
+
272
+ // If it was non-element anchor then we walk through child nodes
273
+ // and looking for a position of original text offset
274
+ node.getChildren().some(_node => {
275
+ const isText = $isTextNode(_node);
276
+ if (isText || $isLineBreakNode(_node)) {
277
+ const textContentSize = _node.getTextContentSize();
278
+ if (isText && textContentSize >= textOffset) {
279
+ _node.select(textOffset, textOffset);
280
+ return true;
281
+ }
282
+ textOffset -= textContentSize;
283
+ }
284
+ return false;
285
+ });
286
+ }
287
+
288
+ // Finds minimal diff range between two nodes lists. It returns from/to range boundaries of prevNodes
289
+ // that needs to be replaced with `nodes` (subset of nextNodes) to make prevNodes equal to nextNodes.
290
+ function getDiffRange(
291
+ prevNodes: Array<LexicalNode>,
292
+ nextNodes: Array<LexicalNode>,
293
+ ): {
294
+ from: number;
295
+ nodesForReplacement: Array<LexicalNode>;
296
+ to: number;
297
+ } {
298
+ let leadingMatch = 0;
299
+ while (leadingMatch < prevNodes.length) {
300
+ if (!isEqual(prevNodes[leadingMatch], nextNodes[leadingMatch])) {
301
+ break;
302
+ }
303
+ leadingMatch++;
304
+ }
305
+
306
+ const prevNodesLength = prevNodes.length;
307
+ const nextNodesLength = nextNodes.length;
308
+ const maxTrailingMatch =
309
+ Math.min(prevNodesLength, nextNodesLength) - leadingMatch;
310
+
311
+ let trailingMatch = 0;
312
+ while (trailingMatch < maxTrailingMatch) {
313
+ trailingMatch++;
314
+ if (
315
+ !isEqual(
316
+ prevNodes[prevNodesLength - trailingMatch],
317
+ nextNodes[nextNodesLength - trailingMatch],
318
+ )
319
+ ) {
320
+ trailingMatch--;
321
+ break;
322
+ }
323
+ }
324
+
325
+ const from = leadingMatch;
326
+ const to = prevNodesLength - trailingMatch;
327
+ const nodesForReplacement = nextNodes.slice(
328
+ leadingMatch,
329
+ nextNodesLength - trailingMatch,
330
+ );
331
+ return {
332
+ from,
333
+ nodesForReplacement,
334
+ to,
335
+ };
336
+ }
337
+
338
+ function isEqual(nodeA: LexicalNode, nodeB: LexicalNode): boolean {
339
+ // Only checking for code highlight nodes, tabs and linebreaks. If it's regular text node
340
+ // returning false so that it's transformed into code highlight node
341
+ return (
342
+ ($isCodeHighlightNode(nodeA) &&
343
+ $isCodeHighlightNode(nodeB) &&
344
+ nodeA.__text === nodeB.__text &&
345
+ nodeA.__highlightType === nodeB.__highlightType &&
346
+ nodeA.__style === nodeB.__style) ||
347
+ ($isTabNode(nodeA) && $isTabNode(nodeB)) ||
348
+ ($isLineBreakNode(nodeA) && $isLineBreakNode(nodeB))
349
+ );
350
+ }
351
+
352
+ /**
353
+ * @internal
354
+ * Register only the Shiki highlighting transforms and the gutter
355
+ * mutation listener. No keyboard / indent handlers — those are the
356
+ * responsibility of
357
+ * {@link "@lexical/code-core".registerCodeIndentation} /
358
+ * {@link "@lexical/code-core".CodeIndentExtension}.
359
+ *
360
+ * Used by {@link CodeShikiExtension}, whose `CodeIndentExtension`
361
+ * dependency handles the indent side. The legacy
362
+ * {@link registerCodeHighlighting} wrapper combines this helper with
363
+ * `registerCodeIndentation` for direct callers that want the original
364
+ * single-call setup.
365
+ *
366
+ * Exported for use by the package's own unit tests; not re-exported
367
+ * from the package entry point.
368
+ */
369
+ export function registerHighlightingOnly(
370
+ editor: LexicalEditor,
371
+ tokenizer: Tokenizer,
372
+ ): () => void {
373
+ const registrations = [];
374
+
375
+ // Only register the mutation listener if not in headless mode
376
+ if (editor._headless !== true) {
377
+ registrations.push(
378
+ editor.registerMutationListener(
379
+ CodeNode,
380
+ mutations => {
381
+ editor.getEditorState().read(() => {
382
+ for (const [key, type] of mutations) {
383
+ if (type !== 'destroyed') {
384
+ const node = $getNodeByKey(key);
385
+ if (node !== null) {
386
+ updateCodeGutter(node as CodeNode, editor);
387
+ }
388
+ }
389
+ }
390
+ });
391
+ },
392
+ {skipInitialization: false},
393
+ ),
394
+ );
395
+ }
396
+
397
+ const transformState: TransformState = {
398
+ didTransform: false,
399
+ nodesCurrentlyHighlighting: new Set(),
400
+ };
401
+ registrations.push(
402
+ editor.registerNodeTransform(
403
+ CodeNode,
404
+ $codeNodeTransform.bind(null, editor, tokenizer, transformState),
405
+ ),
406
+ editor.registerNodeTransform(
407
+ TextNode,
408
+ $textNodeTransform.bind(null, editor, tokenizer, transformState),
409
+ ),
410
+ editor.registerNodeTransform(
411
+ CodeHighlightNode,
412
+ $textNodeTransform.bind(null, editor, tokenizer, transformState),
413
+ ),
414
+ );
415
+
416
+ return mergeRegister(...registrations);
417
+ }
418
+
419
+ /**
420
+ * Register the Shiki tokenizer-driven highlighting on the editor along
421
+ * with the indent / Tab / arrow-key keyboard handlers. This function
422
+ * is provided for legacy code that has not upgraded to using
423
+ * {@link CodeShikiExtension}.
424
+ */
425
+ export function registerCodeHighlighting(
426
+ editor: LexicalEditor,
427
+ tokenizer: Tokenizer = ShikiTokenizer,
428
+ ): () => void {
429
+ if (!editor.hasNodes([CodeNode, CodeHighlightNode])) {
430
+ throw new Error(
431
+ 'CodeHighlightPlugin: CodeNode or CodeHighlightNode not registered on editor',
432
+ );
433
+ }
434
+ return mergeRegister(
435
+ registerHighlightingOnly(editor, tokenizer),
436
+ registerCodeIndentation(editor),
437
+ );
438
+ }
439
+
440
+ export interface CodeShikiConfig {
441
+ /**
442
+ * When true, the Shiki code highlighter is not registered on the editor.
443
+ * This signal can be flipped at runtime to enable or disable the
444
+ * highlighter, for example to switch between the Prism and Shiki
445
+ * highlighters without rebuilding the editor.
446
+ */
447
+ disabled: boolean;
448
+ tokenizer: Tokenizer;
449
+ }
450
+
451
+ /**
452
+ * Add code highlighting support for code blocks with Shiki.
453
+ *
454
+ * {@link CodeExtension} is a dependency, so the required `CodeNode` and
455
+ * `CodeHighlightNode` nodes are registered automatically.
456
+ * {@link CodeIndentExtension} is also a dependency, so Tab / Shift+Tab
457
+ * and the related keyboard handlers are activated automatically. Set
458
+ * `tabSize` on `CodeIndentExtension` to enable space-indent outdent.
459
+ */
460
+ export const CodeShikiExtension = defineExtension({
461
+ build: (editor, config) => namedSignals(config),
462
+ config: safeCast<CodeShikiConfig>({
463
+ disabled: false,
464
+ tokenizer: ShikiTokenizer,
465
+ }),
466
+ dependencies: [CodeExtension, CodeIndentExtension],
467
+ name: '@lexical/code-shiki',
468
+ register: (editor, config, state) => {
469
+ const stores = state.getOutput();
470
+ return effect(() => {
471
+ if (stores.disabled.value) {
472
+ return;
473
+ }
474
+ return registerHighlightingOnly(editor, stores.tokenizer.value);
475
+ });
476
+ },
477
+ });
478
+
479
+ /**
480
+ * @deprecated Use {@link CodeShikiExtension} instead. This type is a
481
+ * flat alias for {@link Tokenizer} kept for backward compatibility with
482
+ * {@link CodeHighlighterShikiExtension}.
483
+ */
484
+ export type CodeHighlighterShikiConfig = Tokenizer;
485
+
486
+ /**
487
+ * @deprecated Use {@link CodeShikiExtension} instead.
488
+ *
489
+ * This is a thin backward-compatibility shim that preserves the original
490
+ * flat {@link Tokenizer} config API. It depends on
491
+ * {@link CodeShikiExtension} and routes its configured tokenizer to the
492
+ * underlying extension during `init` (before `CodeShikiExtension` builds),
493
+ * so consumers using
494
+ * `configExtension(CodeHighlighterShikiExtension, customTokenizer)`
495
+ * continue to work without modification.
496
+ */
497
+ export const CodeHighlighterShikiExtension = defineExtension({
498
+ config: safeCast<CodeHighlighterShikiConfig>(ShikiTokenizer),
499
+ dependencies: [CodeShikiExtension],
500
+ init: (editorConfig, config, state) => {
501
+ // Forward the flat Tokenizer config to CodeShikiExtension's `tokenizer`
502
+ // field before it builds.
503
+ state.getDependency(CodeShikiExtension).config.tokenizer = config;
504
+ },
505
+ name: '@lexical/code-shiki/legacy',
506
+ });
@@ -0,0 +1,219 @@
1
+ /**
2
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ */
8
+
9
+ import type {CodeNode} from '@lexical/code-core';
10
+ import type {ThemedToken, TokensResult} from '@shikijs/types';
11
+ import type {LexicalEditor, LexicalNode, NodeKey} from 'lexical';
12
+
13
+ import {$createCodeHighlightNode, $isCodeNode} from '@lexical/code-core';
14
+ import {
15
+ createHighlighterCoreSync,
16
+ getTokenStyleObject,
17
+ isSpecialLang,
18
+ isSpecialTheme,
19
+ stringifyTokenStyle,
20
+ } from '@shikijs/core';
21
+ import {createJavaScriptRegexEngine} from '@shikijs/engine-javascript';
22
+ import {
23
+ $createLineBreakNode,
24
+ $createTabNode,
25
+ $getNodeByKey,
26
+ tokenizeRawText,
27
+ } from 'lexical';
28
+ import {bundledLanguagesInfo} from 'shiki/langs';
29
+ import {bundledThemesInfo} from 'shiki/themes';
30
+
31
+ const shiki = createHighlighterCoreSync({
32
+ engine: createJavaScriptRegexEngine(),
33
+ langs: [],
34
+ themes: [],
35
+ });
36
+
37
+ function getDiffedLanguage(language: string) {
38
+ const DIFF_LANGUAGE_REGEX = /^diff-([\w-]+)/i;
39
+ const diffLanguageMatch = DIFF_LANGUAGE_REGEX.exec(language);
40
+ return diffLanguageMatch ? diffLanguageMatch[1] : null;
41
+ }
42
+
43
+ export function isCodeLanguageLoaded(language: string) {
44
+ const diffedLanguage = getDiffedLanguage(language);
45
+ const langId = diffedLanguage || language;
46
+
47
+ // handle shiki Hard-coded languages ['ansi', '', 'plaintext', 'txt', 'text', 'plain']
48
+ if (isSpecialLang(langId)) {
49
+ return true;
50
+ }
51
+
52
+ // note: getLoadedLanguages() also returns aliases
53
+ return shiki.getLoadedLanguages().includes(langId);
54
+ }
55
+
56
+ export function loadCodeLanguage(
57
+ language: string,
58
+ editor?: LexicalEditor,
59
+ codeNodeKey?: NodeKey,
60
+ ) {
61
+ const diffedLanguage = getDiffedLanguage(language);
62
+ const langId = diffedLanguage ? diffedLanguage : language;
63
+ if (!isCodeLanguageLoaded(langId)) {
64
+ const languageInfo = bundledLanguagesInfo.find(
65
+ desc =>
66
+ desc.id === langId || (desc.aliases && desc.aliases.includes(langId)),
67
+ );
68
+ if (languageInfo) {
69
+ // in case we arrive here concurrently (not yet loaded language is loaded twice)
70
+ // shiki's synchronous checks make sure to load it only once
71
+ return shiki.loadLanguage(languageInfo.import()).then(() => {
72
+ // here we know that the language is loaded
73
+ // make sure the code is highlighed with the correct language
74
+ if (editor && codeNodeKey) {
75
+ editor.update(() => {
76
+ const codeNode = $getNodeByKey(codeNodeKey);
77
+ if (
78
+ $isCodeNode(codeNode) &&
79
+ codeNode.getLanguage() === language &&
80
+ !codeNode.getIsSyntaxHighlightSupported()
81
+ ) {
82
+ codeNode.setIsSyntaxHighlightSupported(true);
83
+ }
84
+ });
85
+ }
86
+ });
87
+ }
88
+ }
89
+ }
90
+
91
+ export function isCodeThemeLoaded(theme: string) {
92
+ const themeId = theme;
93
+
94
+ // handle shiki special theme ['none']
95
+ if (isSpecialTheme(themeId)) {
96
+ return true;
97
+ }
98
+
99
+ return shiki.getLoadedThemes().includes(themeId);
100
+ }
101
+
102
+ export function loadCodeTheme(
103
+ theme: string,
104
+ editor?: LexicalEditor,
105
+ codeNodeKey?: NodeKey,
106
+ ) {
107
+ if (!isCodeThemeLoaded(theme)) {
108
+ const themeInfo = bundledThemesInfo.find(info => info.id === theme);
109
+ if (themeInfo) {
110
+ return shiki.loadTheme(themeInfo.import()).then(() => {
111
+ if (editor && codeNodeKey) {
112
+ editor.update(() => {
113
+ const codeNode = $getNodeByKey(codeNodeKey);
114
+ if ($isCodeNode(codeNode)) {
115
+ codeNode.markDirty();
116
+ }
117
+ });
118
+ }
119
+ });
120
+ }
121
+ }
122
+ }
123
+
124
+ export function getCodeLanguageOptions(): [string, string][] {
125
+ return bundledLanguagesInfo.map(i => [i.id, i.name]);
126
+ }
127
+ export function getCodeThemeOptions(): [string, string][] {
128
+ return bundledThemesInfo.map(i => [i.id, i.displayName]);
129
+ }
130
+
131
+ export function normalizeCodeLanguage(language: string): string {
132
+ const langId = language;
133
+ const languageInfo = bundledLanguagesInfo.find(
134
+ desc =>
135
+ desc.id === langId || (desc.aliases && desc.aliases.includes(langId)),
136
+ );
137
+ if (languageInfo) {
138
+ return languageInfo.id;
139
+ }
140
+ return language;
141
+ }
142
+
143
+ export function $getHighlightNodes(
144
+ codeNode: CodeNode,
145
+ language: string,
146
+ ): LexicalNode[] {
147
+ const DIFF_LANGUAGE_REGEX = /^diff-([\w-]+)/i;
148
+ const diffLanguageMatch = DIFF_LANGUAGE_REGEX.exec(language);
149
+ const code: string = codeNode.getTextContent();
150
+ const tokensResult: TokensResult = shiki.codeToTokens(code, {
151
+ lang: diffLanguageMatch ? diffLanguageMatch[1] : language,
152
+ theme: codeNode.getTheme() || 'poimandres',
153
+ });
154
+ const {tokens, bg, fg} = tokensResult;
155
+ let style = '';
156
+ if (bg) {
157
+ style += `background-color: ${bg};`;
158
+ }
159
+ if (fg) {
160
+ style += `color: ${fg};`;
161
+ }
162
+ if (codeNode.getStyle() !== style) {
163
+ codeNode.setStyle(style);
164
+ }
165
+ return mapTokensToLexicalStructure(tokens, !!diffLanguageMatch);
166
+ }
167
+
168
+ function mapTokensToLexicalStructure(
169
+ tokens: ThemedToken[][],
170
+ diff: boolean,
171
+ ): LexicalNode[] {
172
+ const nodes: LexicalNode[] = [];
173
+
174
+ tokens.forEach((line, idx) => {
175
+ if (idx) {
176
+ nodes.push($createLineBreakNode());
177
+ }
178
+ line.forEach((token, tidx) => {
179
+ let text = token.content;
180
+
181
+ // implement diff-xxxx languages
182
+ if (diff && tidx === 0 && text.length > 0) {
183
+ const prefixes = ['+', '-', '>', '<', ' '];
184
+ const prefixTypes = [
185
+ 'inserted',
186
+ 'deleted',
187
+ 'inserted',
188
+ 'deleted',
189
+ 'unchanged',
190
+ ];
191
+ const prefixIndex = prefixes.indexOf(text[0]);
192
+ if (prefixIndex !== -1) {
193
+ nodes.push(
194
+ $createCodeHighlightNode(
195
+ prefixes[prefixIndex],
196
+ prefixTypes[prefixIndex],
197
+ ),
198
+ );
199
+ text = text.slice(1);
200
+ }
201
+ }
202
+
203
+ const style = stringifyTokenStyle(
204
+ token.htmlStyle || getTokenStyleObject(token),
205
+ );
206
+ tokenizeRawText(text, {
207
+ linebreak: () => nodes.push($createLineBreakNode()),
208
+ tab: () => nodes.push($createTabNode()),
209
+ text: (part: string) => {
210
+ const node = $createCodeHighlightNode(part);
211
+ node.setStyle(style);
212
+ nodes.push(node);
213
+ },
214
+ });
215
+ });
216
+ });
217
+
218
+ return nodes;
219
+ }
package/src/index.ts ADDED
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ */
8
+
9
+ export {
10
+ type CodeHighlighterShikiConfig,
11
+ CodeHighlighterShikiExtension,
12
+ type CodeShikiConfig,
13
+ CodeShikiExtension,
14
+ registerCodeHighlighting,
15
+ ShikiTokenizer,
16
+ type Tokenizer,
17
+ } from './CodeHighlighterShiki';
18
+ export {
19
+ getCodeLanguageOptions,
20
+ getCodeThemeOptions,
21
+ isCodeLanguageLoaded,
22
+ loadCodeLanguage,
23
+ loadCodeTheme,
24
+ normalizeCodeLanguage,
25
+ } from './FacadeShiki';
@@ -1,9 +0,0 @@
1
- /**
2
- * Copyright (c) Meta Platforms, Inc. and affiliates.
3
- *
4
- * This source code is licensed under the MIT license found in the
5
- * LICENSE file in the root directory of this source tree.
6
- *
7
- */
8
-
9
- "use strict";var e=require("@lexical/code-core"),t=require("@lexical/extension"),n=require("lexical"),i=require("@shikijs/core"),o=require("@shikijs/engine-javascript"),r=require("shiki/langs"),d=require("shiki/themes");const s=i.createHighlighterCoreSync({engine:o.createJavaScriptRegexEngine(),langs:[],themes:[]});function g(e){const t=/^diff-([\w-]+)/i.exec(e);return t?t[1]:null}function a(e){const t=g(e)||e;return!!i.isSpecialLang(t)||s.getLoadedLanguages().includes(t)}function l(t,i,o){const d=g(t),l=d||t;if(!a(l)){const d=r.bundledLanguagesInfo.find(e=>e.id===l||e.aliases&&e.aliases.includes(l));if(d)return s.loadLanguage(d.import()).then(()=>{i&&o&&i.update(()=>{const i=n.$getNodeByKey(o);e.$isCodeNode(i)&&i.getLanguage()===t&&!i.getIsSyntaxHighlightSupported()&&i.setIsSyntaxHighlightSupported(!0)})})}}function u(e){const t=e;return!!i.isSpecialTheme(t)||s.getLoadedThemes().includes(t)}function c(t,i,o){if(!u(t)){const r=d.bundledThemesInfo.find(e=>e.id===t);if(r)return s.loadTheme(r.import()).then(()=>{i&&o&&i.update(()=>{const t=n.$getNodeByKey(o);e.$isCodeNode(t)&&t.markDirty()})})}}function h(t,o){const r=/^diff-([\w-]+)/i.exec(o),d=t.getTextContent(),g=s.codeToTokens(d,{lang:r?r[1]:o,theme:t.getTheme()||"poimandres"}),{tokens:a,bg:l,fg:u}=g;let c="";return l&&(c+=`background-color: ${l};`),u&&(c+=`color: ${u};`),t.getStyle()!==c&&t.setStyle(c),function(t,o){const r=[];return t.forEach((t,d)=>{d&&r.push(n.$createLineBreakNode()),t.forEach((t,d)=>{let s=t.content;if(o&&0===d&&s.length>0){const t=["+","-",">","<"," "],n=["inserted","deleted","inserted","deleted","unchanged"],i=t.indexOf(s[0]);-1!==i&&(r.push(e.$createCodeHighlightNode(t[i],n[i])),s=s.slice(1))}s.split("\t").forEach((o,d)=>{if(d&&r.push(n.$createTabNode()),""!==o){const n=e.$createCodeHighlightNode(o),d=i.stringifyTokenStyle(t.htmlStyle||i.getTokenStyleObject(t));n.setStyle(d),r.push(n)}})})}),r}(a,!!r)}const f={$tokenize(e,t){return h(e,t||this.defaultLanguage)},defaultLanguage:e.DEFAULT_CODE_LANGUAGE,defaultTheme:"one-light"};function p(t,i,o,r){const d=r.getParent();e.$isCodeNode(d)?C(t,i,o,d):e.$isCodeHighlightNode(r)&&r.replace(n.$createTextNode(r.__text))}function m(e,t){const i=t.getElementByKey(e.getKey());if(null===i)return;const o=e.getChildren(),r=o.length;if(r===i.__cachedChildrenLength)return;i.__cachedChildrenLength=r;let d="1",s=1;for(let e=0;e<r;e++)n.$isLineBreakNode(o[e])&&(d+="\n"+ ++s);i.setAttribute("data-gutter",d)}function C(t,i,o,r){const d=r.getKey(),{nodesCurrentlyHighlighting:s}=o;let g=r.getLanguage();g||(g=i.defaultLanguage,r.setLanguage(g));let h=r.getTheme();h||(h=i.defaultTheme,r.setTheme(h));let f=!1;u(h)||(c(h,t,d),f=!0),a(g)?r.getIsSyntaxHighlightSupported()||r.setIsSyntaxHighlightSupported(!0):(r.getIsSyntaxHighlightSupported()&&r.setIsSyntaxHighlightSupported(!1),l(g,t,d),f=!0),f||s.has(d)||(s.add(d),o.didTransform||(o.didTransform=!0,n.$onUpdate(()=>{o.didTransform=!1,s.clear()})),function(t,i){const o=n.$getNodeByKey(t);if(!e.$isCodeNode(o)||!o.isAttached())return;const r=n.$getSelection();if(!n.$isRangeSelection(r))return void i();const d=r.anchor,s=d.offset,g="element"===d.type&&n.$isLineBreakNode(o.getChildAtIndex(d.offset-1));let a=0;if(!g){const e=d.getNode();a=s+e.getPreviousSiblings().reduce((e,t)=>e+t.getTextContentSize(),0)}if(!i())return;if(g)return void d.getNode().select(s,s);o.getChildren().some(e=>{const t=n.$isTextNode(e);if(t||n.$isLineBreakNode(e)){const n=e.getTextContentSize();if(t&&n>=a)return e.select(a,a),!0;a-=n}return!1})}(d,()=>{const t=n.$getNodeByKey(d);if(!e.$isCodeNode(t)||!t.isAttached())return!1;const o=t.getLanguage()||i.defaultLanguage,s=i.$tokenize(t,o),g=function(e,t){let n=0;for(;n<e.length&&x(e[n],t[n]);)n++;const i=e.length,o=t.length,r=Math.min(i,o)-n;let d=0;for(;d<r;)if(d++,!x(e[i-d],t[o-d])){d--;break}const s=n,g=i-d,a=t.slice(n,o-d);return{from:s,nodesForReplacement:a,to:g}}(t.getChildren(),s),{from:a,to:l,nodesForReplacement:u}=g;return!(a===l&&!u.length)&&(r.splice(a,l-a,u),!0)}))}function x(t,i){return e.$isCodeHighlightNode(t)&&e.$isCodeHighlightNode(i)&&t.__text===i.__text&&t.__highlightType===i.__highlightType&&t.__style===i.__style||n.$isTabNode(t)&&n.$isTabNode(i)||n.$isLineBreakNode(t)&&n.$isLineBreakNode(i)}function N(t,i){const o=[];!0!==t._headless&&o.push(t.registerMutationListener(e.CodeNode,e=>{t.getEditorState().read(()=>{for(const[i,o]of e)if("destroyed"!==o){const e=n.$getNodeByKey(i);null!==e&&m(e,t)}})},{skipInitialization:!1}));const r={didTransform:!1,nodesCurrentlyHighlighting:new Set};return o.push(t.registerNodeTransform(e.CodeNode,C.bind(null,t,i,r)),t.registerNodeTransform(n.TextNode,p.bind(null,t,i,r)),t.registerNodeTransform(e.CodeHighlightNode,p.bind(null,t,i,r))),n.mergeRegister(...o)}const y=n.defineExtension({build:(e,n)=>t.namedSignals(n),config:n.safeCast({disabled:!1,tokenizer:f}),dependencies:[e.CodeExtension,e.CodeIndentExtension],name:"@lexical/code-shiki",register:(e,n,i)=>{const o=i.getOutput();return t.effect(()=>{if(!o.disabled.value)return N(e,o.tokenizer.value)})}}),T=n.defineExtension({config:n.safeCast(f),dependencies:[y],init:(e,t,n)=>{n.getDependency(y).config.tokenizer=t},name:"@lexical/code-shiki/legacy"});exports.CodeHighlighterShikiExtension=T,exports.CodeShikiExtension=y,exports.ShikiTokenizer=f,exports.getCodeLanguageOptions=function(){return r.bundledLanguagesInfo.map(e=>[e.id,e.name])},exports.getCodeThemeOptions=function(){return d.bundledThemesInfo.map(e=>[e.id,e.displayName])},exports.isCodeLanguageLoaded=a,exports.loadCodeLanguage=l,exports.loadCodeTheme=c,exports.normalizeCodeLanguage=function(e){const t=e,n=r.bundledLanguagesInfo.find(e=>e.id===t||e.aliases&&e.aliases.includes(t));return n?n.id:e},exports.registerCodeHighlighting=function(t,i=f){if(!t.hasNodes([e.CodeNode,e.CodeHighlightNode]))throw new Error("CodeHighlightPlugin: CodeNode or CodeHighlightNode not registered on editor");return n.mergeRegister(N(t,i),e.registerCodeIndentation(t))};
@@ -1,9 +0,0 @@
1
- /**
2
- * Copyright (c) Meta Platforms, Inc. and affiliates.
3
- *
4
- * This source code is licensed under the MIT license found in the
5
- * LICENSE file in the root directory of this source tree.
6
- *
7
- */
8
-
9
- import{$isCodeNode as e,$createCodeHighlightNode as t,CodeExtension as n,CodeIndentExtension as i,DEFAULT_CODE_LANGUAGE as o,CodeNode as r,CodeHighlightNode as s,$isCodeHighlightNode as l,registerCodeIndentation as d}from"@lexical/code-core";import{effect as a,namedSignals as g}from"@lexical/extension";import{$getNodeByKey as c,$createLineBreakNode as u,$createTabNode as f,defineExtension as h,safeCast as m,TextNode as p,mergeRegister as y,$isLineBreakNode as x,$onUpdate as S,$createTextNode as T,$getSelection as _,$isRangeSelection as k,$isTextNode as L,$isTabNode as C}from"lexical";import{createHighlighterCoreSync as b,isSpecialTheme as H,isSpecialLang as N,stringifyTokenStyle as z,getTokenStyleObject as I}from"@shikijs/core";import{createJavaScriptRegexEngine as v}from"@shikijs/engine-javascript";import{bundledLanguagesInfo as E}from"shiki/langs";import{bundledThemesInfo as w}from"shiki/themes";const A=b({engine:v(),langs:[],themes:[]});function $(e){const t=/^diff-([\w-]+)/i.exec(e);return t?t[1]:null}function j(e){const t=$(e)||e;return!!N(t)||A.getLoadedLanguages().includes(t)}function K(t,n,i){const o=$(t),r=o||t;if(!j(r)){const o=E.find(e=>e.id===r||e.aliases&&e.aliases.includes(r));if(o)return A.loadLanguage(o.import()).then(()=>{n&&i&&n.update(()=>{const n=c(i);e(n)&&n.getLanguage()===t&&!n.getIsSyntaxHighlightSupported()&&n.setIsSyntaxHighlightSupported(!0)})})}}function P(e){const t=e;return!!H(t)||A.getLoadedThemes().includes(t)}function D(t,n,i){if(!P(t)){const o=w.find(e=>e.id===t);if(o)return A.loadTheme(o.import()).then(()=>{n&&i&&n.update(()=>{const t=c(i);e(t)&&t.markDirty()})})}}function F(){return E.map(e=>[e.id,e.name])}function M(){return w.map(e=>[e.id,e.displayName])}function O(e){const t=e,n=E.find(e=>e.id===t||e.aliases&&e.aliases.includes(t));return n?n.id:e}function R(e,n){const i=/^diff-([\w-]+)/i.exec(n),o=e.getTextContent(),r=A.codeToTokens(o,{lang:i?i[1]:n,theme:e.getTheme()||"poimandres"}),{tokens:s,bg:l,fg:d}=r;let a="";return l&&(a+=`background-color: ${l};`),d&&(a+=`color: ${d};`),e.getStyle()!==a&&e.setStyle(a),function(e,n){const i=[];return e.forEach((e,o)=>{o&&i.push(u()),e.forEach((e,o)=>{let r=e.content;if(n&&0===o&&r.length>0){const e=["+","-",">","<"," "],n=["inserted","deleted","inserted","deleted","unchanged"],o=e.indexOf(r[0]);-1!==o&&(i.push(t(e[o],n[o])),r=r.slice(1))}r.split("\t").forEach((n,o)=>{if(o&&i.push(f()),""!==n){const o=t(n),r=z(e.htmlStyle||I(e));o.setStyle(r),i.push(o)}})})}),i}(s,!!i)}const B={$tokenize(e,t){return R(e,t||this.defaultLanguage)},defaultLanguage:o,defaultTheme:"one-light"};function q(t,n,i,o){const r=o.getParent();e(r)?J(t,n,i,r):l(o)&&o.replace(T(o.__text))}function G(e,t){const n=t.getElementByKey(e.getKey());if(null===n)return;const i=e.getChildren(),o=i.length;if(o===n.__cachedChildrenLength)return;n.__cachedChildrenLength=o;let r="1",s=1;for(let e=0;e<o;e++)x(i[e])&&(r+="\n"+ ++s);n.setAttribute("data-gutter",r)}function J(t,n,i,o){const r=o.getKey(),{nodesCurrentlyHighlighting:s}=i;let l=o.getLanguage();l||(l=n.defaultLanguage,o.setLanguage(l));let d=o.getTheme();d||(d=n.defaultTheme,o.setTheme(d));let a=!1;P(d)||(D(d,t,r),a=!0),j(l)?o.getIsSyntaxHighlightSupported()||o.setIsSyntaxHighlightSupported(!0):(o.getIsSyntaxHighlightSupported()&&o.setIsSyntaxHighlightSupported(!1),K(l,t,r),a=!0),a||s.has(r)||(s.add(r),i.didTransform||(i.didTransform=!0,S(()=>{i.didTransform=!1,s.clear()})),function(t,n){const i=c(t);if(!e(i)||!i.isAttached())return;const o=_();if(!k(o))return void n();const r=o.anchor,s=r.offset,l="element"===r.type&&x(i.getChildAtIndex(r.offset-1));let d=0;if(!l){const e=r.getNode();d=s+e.getPreviousSiblings().reduce((e,t)=>e+t.getTextContentSize(),0)}if(!n())return;if(l)return void r.getNode().select(s,s);i.getChildren().some(e=>{const t=L(e);if(t||x(e)){const n=e.getTextContentSize();if(t&&n>=d)return e.select(d,d),!0;d-=n}return!1})}(r,()=>{const t=c(r);if(!e(t)||!t.isAttached())return!1;const i=t.getLanguage()||n.defaultLanguage,s=n.$tokenize(t,i),l=function(e,t){let n=0;for(;n<e.length&&Q(e[n],t[n]);)n++;const i=e.length,o=t.length,r=Math.min(i,o)-n;let s=0;for(;s<r;)if(s++,!Q(e[i-s],t[o-s])){s--;break}const l=n,d=i-s,a=t.slice(n,o-s);return{from:l,nodesForReplacement:a,to:d}}(t.getChildren(),s),{from:d,to:a,nodesForReplacement:g}=l;return!(d===a&&!g.length)&&(o.splice(d,a-d,g),!0)}))}function Q(e,t){return l(e)&&l(t)&&e.__text===t.__text&&e.__highlightType===t.__highlightType&&e.__style===t.__style||C(e)&&C(t)||x(e)&&x(t)}function U(e,t){const n=[];!0!==e._headless&&n.push(e.registerMutationListener(r,t=>{e.getEditorState().read(()=>{for(const[n,i]of t)if("destroyed"!==i){const t=c(n);null!==t&&G(t,e)}})},{skipInitialization:!1}));const i={didTransform:!1,nodesCurrentlyHighlighting:new Set};return n.push(e.registerNodeTransform(r,J.bind(null,e,t,i)),e.registerNodeTransform(p,q.bind(null,e,t,i)),e.registerNodeTransform(s,q.bind(null,e,t,i))),y(...n)}function V(e,t=B){if(!e.hasNodes([r,s]))throw new Error("CodeHighlightPlugin: CodeNode or CodeHighlightNode not registered on editor");return y(U(e,t),d(e))}const W=h({build:(e,t)=>g(t),config:m({disabled:!1,tokenizer:B}),dependencies:[n,i],name:"@lexical/code-shiki",register:(e,t,n)=>{const i=n.getOutput();return a(()=>{if(!i.disabled.value)return U(e,i.tokenizer.value)})}}),X=h({config:m(B),dependencies:[W],init:(e,t,n)=>{n.getDependency(W).config.tokenizer=t},name:"@lexical/code-shiki/legacy"});export{X as CodeHighlighterShikiExtension,W as CodeShikiExtension,B as ShikiTokenizer,F as getCodeLanguageOptions,M as getCodeThemeOptions,j as isCodeLanguageLoaded,K as loadCodeLanguage,D as loadCodeTheme,O as normalizeCodeLanguage,V as registerCodeHighlighting};
File without changes
File without changes