@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.
- package/{CodeHighlighterShiki.d.ts → dist/CodeHighlighterShiki.d.ts} +8 -1
- package/{LexicalCodeShiki.dev.js → dist/LexicalCodeShiki.dev.js} +34 -21
- package/{LexicalCodeShiki.dev.mjs → dist/LexicalCodeShiki.dev.mjs} +36 -23
- 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 +219 -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
|
}
|
|
@@ -153,14 +153,12 @@ function mapTokensToLexicalStructure(tokens, diff) {
|
|
|
153
153
|
text = text.slice(1);
|
|
154
154
|
}
|
|
155
155
|
}
|
|
156
|
-
const
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
251
|
-
if (
|
|
252
|
-
node.
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
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;
|
|
257
270
|
}
|
|
258
|
-
|
|
259
|
-
|
|
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
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
249
|
-
if (
|
|
250
|
-
node.
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
node.
|
|
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
|
-
|
|
257
|
-
|
|
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,
|
|
@@ -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.
|
|
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
|
-
"
|
|
22
|
-
"
|
|
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';
|
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
|