@lexical/code-shiki 0.44.1-nightly.20260518.0 → 0.45.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.
- package/{CodeHighlighterShiki.d.ts → dist/CodeHighlighterShiki.d.ts} +8 -1
- package/{LexicalCodeShiki.dev.js → dist/LexicalCodeShiki.dev.js} +29 -14
- package/{LexicalCodeShiki.dev.mjs → dist/LexicalCodeShiki.dev.mjs} +30 -15
- package/{LexicalCodeShiki.js.flow → dist/LexicalCodeShiki.js.flow} +1 -1
- package/dist/LexicalCodeShiki.prod.js +9 -0
- package/dist/LexicalCodeShiki.prod.mjs +9 -0
- package/package.json +32 -18
- package/src/CodeHighlighterShiki.ts +506 -0
- package/src/FacadeShiki.ts +216 -0
- package/src/index.ts +25 -0
- package/LexicalCodeShiki.prod.js +0 -9
- package/LexicalCodeShiki.prod.mjs +0 -9
- /package/{FacadeShiki.d.ts → dist/FacadeShiki.d.ts} +0 -0
- /package/{LexicalCodeShiki.js → dist/LexicalCodeShiki.js} +0 -0
- /package/{LexicalCodeShiki.mjs → dist/LexicalCodeShiki.mjs} +0 -0
- /package/{LexicalCodeShiki.node.mjs → dist/LexicalCodeShiki.node.mjs} +0 -0
- /package/{index.d.ts → dist/index.d.ts} +0 -0
|
@@ -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
|
-
|
|
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
|
}
|
|
@@ -181,7 +181,8 @@ function mapTokensToLexicalStructure(tokens, diff) {
|
|
|
181
181
|
const DEFAULT_CODE_THEME = 'one-light';
|
|
182
182
|
const ShikiTokenizer = {
|
|
183
183
|
$tokenize(codeNode, language) {
|
|
184
|
-
|
|
184
|
+
const lang = language || this.defaultLanguage;
|
|
185
|
+
return lang === null ? codeCore.$plainifyCodeContent(codeNode.getTextContent()) : $getHighlightNodes(codeNode, lang);
|
|
185
186
|
},
|
|
186
187
|
defaultLanguage: codeCore.DEFAULT_CODE_LANGUAGE,
|
|
187
188
|
defaultTheme: DEFAULT_CODE_THEME
|
|
@@ -227,9 +228,12 @@ function $codeNodeTransform(editor, tokenizer, transformState, node) {
|
|
|
227
228
|
nodesCurrentlyHighlighting
|
|
228
229
|
} = transformState;
|
|
229
230
|
|
|
230
|
-
// When new code block inserted it might not have language selected
|
|
231
|
+
// When new code block inserted it might not have language selected.
|
|
232
|
+
// Tokenizers configured with `defaultLanguage: null` opt out of the
|
|
233
|
+
// implicit fallback — leave the node unset and skip highlighting so
|
|
234
|
+
// markdown round-trips ``` (no info string) without injecting one.
|
|
231
235
|
let language = node.getLanguage();
|
|
232
|
-
if (!language) {
|
|
236
|
+
if (!language && tokenizer.defaultLanguage !== null) {
|
|
233
237
|
language = tokenizer.defaultLanguage;
|
|
234
238
|
node.setLanguage(language);
|
|
235
239
|
}
|
|
@@ -243,20 +247,31 @@ function $codeNodeTransform(editor, tokenizer, transformState, node) {
|
|
|
243
247
|
let inFlight = false;
|
|
244
248
|
if (!isCodeThemeLoaded(theme)) {
|
|
245
249
|
loadCodeTheme(theme, editor, nodeKey);
|
|
246
|
-
|
|
250
|
+
// Only the highlight path (a resolved language) consumes the theme. With
|
|
251
|
+
// no language the text is plainified, which needs no theme, so don't defer
|
|
252
|
+
// the split on a theme load that won't be used — otherwise a code block
|
|
253
|
+
// with `defaultLanguage: null` stays an unsplit TextNode until the theme
|
|
254
|
+
// happens to finish loading.
|
|
255
|
+
if (language) {
|
|
256
|
+
inFlight = true;
|
|
257
|
+
}
|
|
247
258
|
}
|
|
248
259
|
|
|
249
260
|
// dynamic import of languages
|
|
250
|
-
if (
|
|
251
|
-
if (
|
|
252
|
-
node.
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
node.
|
|
261
|
+
if (language) {
|
|
262
|
+
if (isCodeLanguageLoaded(language)) {
|
|
263
|
+
if (!node.getIsSyntaxHighlightSupported()) {
|
|
264
|
+
node.setIsSyntaxHighlightSupported(true);
|
|
265
|
+
}
|
|
266
|
+
} else {
|
|
267
|
+
if (node.getIsSyntaxHighlightSupported()) {
|
|
268
|
+
node.setIsSyntaxHighlightSupported(false);
|
|
269
|
+
}
|
|
270
|
+
loadCodeLanguage(language, editor, nodeKey);
|
|
271
|
+
inFlight = true;
|
|
257
272
|
}
|
|
258
|
-
|
|
259
|
-
|
|
273
|
+
} else if (node.getIsSyntaxHighlightSupported()) {
|
|
274
|
+
node.setIsSyntaxHighlightSupported(false);
|
|
260
275
|
}
|
|
261
276
|
if (inFlight) {
|
|
262
277
|
return;
|
|
@@ -278,7 +293,7 @@ function $codeNodeTransform(editor, tokenizer, transformState, node) {
|
|
|
278
293
|
return false;
|
|
279
294
|
}
|
|
280
295
|
const lang = currentNode.getLanguage() || tokenizer.defaultLanguage;
|
|
281
|
-
const highlightNodes = tokenizer.$tokenize(currentNode, lang);
|
|
296
|
+
const highlightNodes = tokenizer.$tokenize(currentNode, lang ?? undefined);
|
|
282
297
|
const diffRange = getDiffRange(currentNode.getChildren(), highlightNodes);
|
|
283
298
|
const {
|
|
284
299
|
from,
|
|
@@ -6,7 +6,7 @@
|
|
|
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
11
|
import { $getNodeByKey, $createLineBreakNode, $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';
|
|
@@ -179,7 +179,8 @@ function mapTokensToLexicalStructure(tokens, diff) {
|
|
|
179
179
|
const DEFAULT_CODE_THEME = 'one-light';
|
|
180
180
|
const ShikiTokenizer = {
|
|
181
181
|
$tokenize(codeNode, language) {
|
|
182
|
-
|
|
182
|
+
const lang = language || this.defaultLanguage;
|
|
183
|
+
return lang === null ? $plainifyCodeContent(codeNode.getTextContent()) : $getHighlightNodes(codeNode, lang);
|
|
183
184
|
},
|
|
184
185
|
defaultLanguage: DEFAULT_CODE_LANGUAGE,
|
|
185
186
|
defaultTheme: DEFAULT_CODE_THEME
|
|
@@ -225,9 +226,12 @@ function $codeNodeTransform(editor, tokenizer, transformState, node) {
|
|
|
225
226
|
nodesCurrentlyHighlighting
|
|
226
227
|
} = transformState;
|
|
227
228
|
|
|
228
|
-
// 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.
|
|
229
233
|
let language = node.getLanguage();
|
|
230
|
-
if (!language) {
|
|
234
|
+
if (!language && tokenizer.defaultLanguage !== null) {
|
|
231
235
|
language = tokenizer.defaultLanguage;
|
|
232
236
|
node.setLanguage(language);
|
|
233
237
|
}
|
|
@@ -241,20 +245,31 @@ function $codeNodeTransform(editor, tokenizer, transformState, node) {
|
|
|
241
245
|
let inFlight = false;
|
|
242
246
|
if (!isCodeThemeLoaded(theme)) {
|
|
243
247
|
loadCodeTheme(theme, editor, nodeKey);
|
|
244
|
-
|
|
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
|
+
}
|
|
245
256
|
}
|
|
246
257
|
|
|
247
258
|
// dynamic import of languages
|
|
248
|
-
if (
|
|
249
|
-
if (
|
|
250
|
-
node.
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
node.
|
|
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;
|
|
255
270
|
}
|
|
256
|
-
|
|
257
|
-
|
|
271
|
+
} else if (node.getIsSyntaxHighlightSupported()) {
|
|
272
|
+
node.setIsSyntaxHighlightSupported(false);
|
|
258
273
|
}
|
|
259
274
|
if (inFlight) {
|
|
260
275
|
return;
|
|
@@ -276,7 +291,7 @@ function $codeNodeTransform(editor, tokenizer, transformState, node) {
|
|
|
276
291
|
return false;
|
|
277
292
|
}
|
|
278
293
|
const lang = currentNode.getLanguage() || tokenizer.defaultLanguage;
|
|
279
|
-
const highlightNodes = tokenizer.$tokenize(currentNode, lang);
|
|
294
|
+
const highlightNodes = tokenizer.$tokenize(currentNode, lang ?? undefined);
|
|
280
295
|
const diffRange = getDiffRange(currentNode.getChildren(), highlightNodes);
|
|
281
296
|
const {
|
|
282
297
|
from,
|
|
@@ -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))}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(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)?m(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 m(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&&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 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,m.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)})}}),S=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=S,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 f,$createTabNode as h,defineExtension as p,safeCast as m,TextNode as y,mergeRegister as x,$isLineBreakNode as S,$onUpdate as T,$createTextNode as _,$getSelection as k,$isRangeSelection as L,$isTextNode as C,$isTabNode as H}from"lexical";import{createHighlighterCoreSync as b,isSpecialTheme as I,isSpecialLang as N,stringifyTokenStyle as z,getTokenStyleObject as v}from"@shikijs/core";import{createJavaScriptRegexEngine as E}from"@shikijs/engine-javascript";import{bundledLanguagesInfo as w}from"shiki/langs";import{bundledThemesInfo as A}from"shiki/themes";const $=b({engine:E(),langs:[],themes:[]});function j(e){const t=/^diff-([\w-]+)/i.exec(e);return t?t[1]:null}function K(e){const t=j(e)||e;return!!N(t)||$.getLoadedLanguages().includes(t)}function P(t,n,i){const o=j(t),r=o||t;if(!K(r)){const o=w.find(e=>e.id===r||e.aliases&&e.aliases.includes(r));if(o)return $.loadLanguage(o.import()).then(()=>{n&&i&&n.update(()=>{const n=c(i);e(n)&&n.getLanguage()===t&&!n.getIsSyntaxHighlightSupported()&&n.setIsSyntaxHighlightSupported(!0)})})}}function D(e){const t=e;return!!I(t)||$.getLoadedThemes().includes(t)}function F(t,n,i){if(!D(t)){const o=A.find(e=>e.id===t);if(o)return $.loadTheme(o.import()).then(()=>{n&&i&&n.update(()=>{const t=c(i);e(t)&&t.markDirty()})})}}function M(){return w.map(e=>[e.id,e.name])}function O(){return A.map(e=>[e.id,e.displayName])}function R(e){const t=e,n=w.find(e=>e.id===t||e.aliases&&e.aliases.includes(t));return n?n.id:e}function B(e,n){const i=/^diff-([\w-]+)/i.exec(n),o=e.getTextContent(),r=$.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(f()),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(h()),""!==n){const o=t(n),r=z(e.htmlStyle||v(e));o.setStyle(r),i.push(o)}})})}),i}(s,!!i)}const q={$tokenize(e,t){const n=t||this.defaultLanguage;return null===n?l(e.getTextContent()):B(e,n)},defaultLanguage:o,defaultTheme:"one-light"};function G(t,n,i,o){const r=o.getParent();e(r)?Q(t,n,i,r):d(o)&&o.replace(_(o.__text))}function J(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++)S(i[e])&&(r+="\n"+ ++s);n.setAttribute("data-gutter",r)}function Q(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;D(d)||(F(d,t,r),l&&(g=!0)),l?K(l)?o.getIsSyntaxHighlightSupported()||o.setIsSyntaxHighlightSupported(!0):(o.getIsSyntaxHighlightSupported()&&o.setIsSyntaxHighlightSupported(!1),P(l,t,r),g=!0):o.getIsSyntaxHighlightSupported()&&o.setIsSyntaxHighlightSupported(!1),g||s.has(r)||(s.add(r),i.didTransform||(i.didTransform=!0,T(()=>{i.didTransform=!1,s.clear()})),function(t,n){const i=c(t);if(!e(i)||!i.isAttached())return;const o=k();if(!L(o))return void n();const r=o.anchor,s=r.offset,l="element"===r.type&&S(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=C(e);if(t||S(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&&U(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++,!U(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 U(e,t){return d(e)&&d(t)&&e.__text===t.__text&&e.__highlightType===t.__highlightType&&e.__style===t.__style||H(e)&&H(t)||S(e)&&S(t)}function V(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&&J(t,e)}})},{skipInitialization:!1}));const i={didTransform:!1,nodesCurrentlyHighlighting:new Set};return n.push(e.registerNodeTransform(r,Q.bind(null,e,t,i)),e.registerNodeTransform(y,G.bind(null,e,t,i)),e.registerNodeTransform(s,G.bind(null,e,t,i))),x(...n)}function W(e,t=q){if(!e.hasNodes([r,s]))throw new Error("CodeHighlightPlugin: CodeNode or CodeHighlightNode not registered on editor");return x(V(e,t),g(e))}const X=p({build:(e,t)=>u(t),config:m({disabled:!1,tokenizer:q}),dependencies:[n,i],name:"@lexical/code-shiki",register:(e,t,n)=>{const i=n.getOutput();return a(()=>{if(!i.disabled.value)return V(e,i.tokenizer.value)})}}),Y=p({config:m(q),dependencies:[X],init:(e,t,n)=>{n.getDependency(X).config.tokenizer=t},name:"@lexical/code-shiki/legacy"});export{Y as CodeHighlighterShikiExtension,X as CodeShikiExtension,q as ShikiTokenizer,M as getCodeLanguageOptions,O as getCodeThemeOptions,K as isCodeLanguageLoaded,P as loadCodeLanguage,F as loadCodeTheme,R as normalizeCodeLanguage,W as registerCodeHighlighting};
|
package/package.json
CHANGED
|
@@ -8,18 +8,18 @@
|
|
|
8
8
|
"code"
|
|
9
9
|
],
|
|
10
10
|
"license": "MIT",
|
|
11
|
-
"version": "0.
|
|
12
|
-
"main": "LexicalCodeShiki.js",
|
|
13
|
-
"types": "index.d.ts",
|
|
11
|
+
"version": "0.45.0",
|
|
12
|
+
"main": "./dist/LexicalCodeShiki.js",
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
14
|
"dependencies": {
|
|
15
|
-
"@lexical/code-core": "0.44.1-nightly.20260518.0",
|
|
16
|
-
"@lexical/extension": "0.44.1-nightly.20260518.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
|
-
"
|
|
22
|
-
"
|
|
19
|
+
"shiki": "^4.0.2",
|
|
20
|
+
"@lexical/code-core": "0.45.0",
|
|
21
|
+
"lexical": "0.45.0",
|
|
22
|
+
"@lexical/extension": "0.45.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,216 @@
|
|
|
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 {$createLineBreakNode, $createTabNode, $getNodeByKey} from 'lexical';
|
|
23
|
+
import {bundledLanguagesInfo} from 'shiki/langs';
|
|
24
|
+
import {bundledThemesInfo} from 'shiki/themes';
|
|
25
|
+
|
|
26
|
+
const shiki = createHighlighterCoreSync({
|
|
27
|
+
engine: createJavaScriptRegexEngine(),
|
|
28
|
+
langs: [],
|
|
29
|
+
themes: [],
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
function getDiffedLanguage(language: string) {
|
|
33
|
+
const DIFF_LANGUAGE_REGEX = /^diff-([\w-]+)/i;
|
|
34
|
+
const diffLanguageMatch = DIFF_LANGUAGE_REGEX.exec(language);
|
|
35
|
+
return diffLanguageMatch ? diffLanguageMatch[1] : null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function isCodeLanguageLoaded(language: string) {
|
|
39
|
+
const diffedLanguage = getDiffedLanguage(language);
|
|
40
|
+
const langId = diffedLanguage || language;
|
|
41
|
+
|
|
42
|
+
// handle shiki Hard-coded languages ['ansi', '', 'plaintext', 'txt', 'text', 'plain']
|
|
43
|
+
if (isSpecialLang(langId)) {
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// note: getLoadedLanguages() also returns aliases
|
|
48
|
+
return shiki.getLoadedLanguages().includes(langId);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function loadCodeLanguage(
|
|
52
|
+
language: string,
|
|
53
|
+
editor?: LexicalEditor,
|
|
54
|
+
codeNodeKey?: NodeKey,
|
|
55
|
+
) {
|
|
56
|
+
const diffedLanguage = getDiffedLanguage(language);
|
|
57
|
+
const langId = diffedLanguage ? diffedLanguage : language;
|
|
58
|
+
if (!isCodeLanguageLoaded(langId)) {
|
|
59
|
+
const languageInfo = bundledLanguagesInfo.find(
|
|
60
|
+
desc =>
|
|
61
|
+
desc.id === langId || (desc.aliases && desc.aliases.includes(langId)),
|
|
62
|
+
);
|
|
63
|
+
if (languageInfo) {
|
|
64
|
+
// in case we arrive here concurrently (not yet loaded language is loaded twice)
|
|
65
|
+
// shiki's synchronous checks make sure to load it only once
|
|
66
|
+
return shiki.loadLanguage(languageInfo.import()).then(() => {
|
|
67
|
+
// here we know that the language is loaded
|
|
68
|
+
// make sure the code is highlighed with the correct language
|
|
69
|
+
if (editor && codeNodeKey) {
|
|
70
|
+
editor.update(() => {
|
|
71
|
+
const codeNode = $getNodeByKey(codeNodeKey);
|
|
72
|
+
if (
|
|
73
|
+
$isCodeNode(codeNode) &&
|
|
74
|
+
codeNode.getLanguage() === language &&
|
|
75
|
+
!codeNode.getIsSyntaxHighlightSupported()
|
|
76
|
+
) {
|
|
77
|
+
codeNode.setIsSyntaxHighlightSupported(true);
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function isCodeThemeLoaded(theme: string) {
|
|
87
|
+
const themeId = theme;
|
|
88
|
+
|
|
89
|
+
// handle shiki special theme ['none']
|
|
90
|
+
if (isSpecialTheme(themeId)) {
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return shiki.getLoadedThemes().includes(themeId);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function loadCodeTheme(
|
|
98
|
+
theme: string,
|
|
99
|
+
editor?: LexicalEditor,
|
|
100
|
+
codeNodeKey?: NodeKey,
|
|
101
|
+
) {
|
|
102
|
+
if (!isCodeThemeLoaded(theme)) {
|
|
103
|
+
const themeInfo = bundledThemesInfo.find(info => info.id === theme);
|
|
104
|
+
if (themeInfo) {
|
|
105
|
+
return shiki.loadTheme(themeInfo.import()).then(() => {
|
|
106
|
+
if (editor && codeNodeKey) {
|
|
107
|
+
editor.update(() => {
|
|
108
|
+
const codeNode = $getNodeByKey(codeNodeKey);
|
|
109
|
+
if ($isCodeNode(codeNode)) {
|
|
110
|
+
codeNode.markDirty();
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function getCodeLanguageOptions(): [string, string][] {
|
|
120
|
+
return bundledLanguagesInfo.map(i => [i.id, i.name]);
|
|
121
|
+
}
|
|
122
|
+
export function getCodeThemeOptions(): [string, string][] {
|
|
123
|
+
return bundledThemesInfo.map(i => [i.id, i.displayName]);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function normalizeCodeLanguage(language: string): string {
|
|
127
|
+
const langId = language;
|
|
128
|
+
const languageInfo = bundledLanguagesInfo.find(
|
|
129
|
+
desc =>
|
|
130
|
+
desc.id === langId || (desc.aliases && desc.aliases.includes(langId)),
|
|
131
|
+
);
|
|
132
|
+
if (languageInfo) {
|
|
133
|
+
return languageInfo.id;
|
|
134
|
+
}
|
|
135
|
+
return language;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function $getHighlightNodes(
|
|
139
|
+
codeNode: CodeNode,
|
|
140
|
+
language: string,
|
|
141
|
+
): LexicalNode[] {
|
|
142
|
+
const DIFF_LANGUAGE_REGEX = /^diff-([\w-]+)/i;
|
|
143
|
+
const diffLanguageMatch = DIFF_LANGUAGE_REGEX.exec(language);
|
|
144
|
+
const code: string = codeNode.getTextContent();
|
|
145
|
+
const tokensResult: TokensResult = shiki.codeToTokens(code, {
|
|
146
|
+
lang: diffLanguageMatch ? diffLanguageMatch[1] : language,
|
|
147
|
+
theme: codeNode.getTheme() || 'poimandres',
|
|
148
|
+
});
|
|
149
|
+
const {tokens, bg, fg} = tokensResult;
|
|
150
|
+
let style = '';
|
|
151
|
+
if (bg) {
|
|
152
|
+
style += `background-color: ${bg};`;
|
|
153
|
+
}
|
|
154
|
+
if (fg) {
|
|
155
|
+
style += `color: ${fg};`;
|
|
156
|
+
}
|
|
157
|
+
if (codeNode.getStyle() !== style) {
|
|
158
|
+
codeNode.setStyle(style);
|
|
159
|
+
}
|
|
160
|
+
return mapTokensToLexicalStructure(tokens, !!diffLanguageMatch);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function mapTokensToLexicalStructure(
|
|
164
|
+
tokens: ThemedToken[][],
|
|
165
|
+
diff: boolean,
|
|
166
|
+
): LexicalNode[] {
|
|
167
|
+
const nodes: LexicalNode[] = [];
|
|
168
|
+
|
|
169
|
+
tokens.forEach((line, idx) => {
|
|
170
|
+
if (idx) {
|
|
171
|
+
nodes.push($createLineBreakNode());
|
|
172
|
+
}
|
|
173
|
+
line.forEach((token, tidx) => {
|
|
174
|
+
let text = token.content;
|
|
175
|
+
|
|
176
|
+
// implement diff-xxxx languages
|
|
177
|
+
if (diff && tidx === 0 && text.length > 0) {
|
|
178
|
+
const prefixes = ['+', '-', '>', '<', ' '];
|
|
179
|
+
const prefixTypes = [
|
|
180
|
+
'inserted',
|
|
181
|
+
'deleted',
|
|
182
|
+
'inserted',
|
|
183
|
+
'deleted',
|
|
184
|
+
'unchanged',
|
|
185
|
+
];
|
|
186
|
+
const prefixIndex = prefixes.indexOf(text[0]);
|
|
187
|
+
if (prefixIndex !== -1) {
|
|
188
|
+
nodes.push(
|
|
189
|
+
$createCodeHighlightNode(
|
|
190
|
+
prefixes[prefixIndex],
|
|
191
|
+
prefixTypes[prefixIndex],
|
|
192
|
+
),
|
|
193
|
+
);
|
|
194
|
+
text = text.slice(1);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const parts = text.split('\t');
|
|
199
|
+
parts.forEach((part: string, pidx: number) => {
|
|
200
|
+
if (pidx) {
|
|
201
|
+
nodes.push($createTabNode());
|
|
202
|
+
}
|
|
203
|
+
if (part !== '') {
|
|
204
|
+
const node = $createCodeHighlightNode(part);
|
|
205
|
+
const style = stringifyTokenStyle(
|
|
206
|
+
token.htmlStyle || getTokenStyleObject(token),
|
|
207
|
+
);
|
|
208
|
+
node.setStyle(style);
|
|
209
|
+
nodes.push(node);
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
return nodes;
|
|
216
|
+
}
|
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';
|
package/LexicalCodeShiki.prod.js
DELETED
|
@@ -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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|