Skip to content

Commit 3bbc356

Browse files
committed
refactor(CodeBlock): store lowlight instance in separate plugin's state
1 parent 27d023d commit 3bbc356

File tree

4 files changed

+155
-107
lines changed

4 files changed

+155
-107
lines changed

packages/editor/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
"test:cov": "jest --coverage",
2727
"test:watch": "jest --watchAll",
2828
"test:esbuild": "node tests/esbuild-test/esbuild-tester.js",
29-
"test:circular-deps": "node scripts/check-circular-deps.js 48",
29+
"test:circular-deps": "node scripts/check-circular-deps.js 47",
3030
"prepack": "cp ../../README.md ./README.md",
3131
"postpack": "rm -f ./README.md",
3232
"prepublishOnly": "pnpm run lint && pnpm run clean && pnpm run build"
Lines changed: 32 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,11 @@
1-
import type {Options} from '@diplodoc/transform';
2-
// importing only type, because lowlight and highlight.js is optional deps
3-
import type HLJS from 'highlight.js/lib/core';
4-
import type {createLowlight} from 'lowlight' with {'resolution-mode': 'import'};
51
import type {Node} from 'prosemirror-model';
62
import {Plugin, PluginKey} from 'prosemirror-state';
73
// @ts-ignore // TODO: fix cjs build
84
import {findChildrenByType} from 'prosemirror-utils';
9-
import {Decoration, DecorationSet, type EditorView} from 'prosemirror-view';
5+
import {Decoration, DecorationSet} from 'prosemirror-view';
6+
7+
import type {ExtensionAuto} from '#core';
108

11-
import type {ExtensionAuto} from '../../../../core';
12-
import {capitalize} from '../../../../lodash';
13-
import {globalLogger} from '../../../../logger';
149
import {
1510
CodeBlockNodeAttr,
1611
type LineNumbersOptions,
@@ -20,22 +15,21 @@ import {
2015

2116
import {CodeBlockNodeView} from './CodeBlockNodeView';
2217
import {codeLangSelectTooltipViewCreator} from './TooltipPlugin';
23-
import {PlainTextLang} from './const';
18+
import {
19+
type HighlightLangMap,
20+
type LLRoot,
21+
type Lowlight,
22+
codeBlockLangsPlugin,
23+
codeBlockLangsPluginKey,
24+
getCodeBlockLangsState,
25+
} from './plugins/codeBlockLangsPlugin';
2426
import {codeBlockLineNumbersPlugin} from './plugins/codeBlockLineNumbersPlugin';
2527
import {codeBlockLineWrappingPlugin} from './plugins/codeBlockLineWrappingPlugin';
2628
import {processChangedCodeBlocks} from './utils';
2729

2830
import './CodeBlockHighlight.scss';
2931

30-
export type HighlightLangMap = Options['highlightLangs'];
31-
32-
type Lowlight = ReturnType<typeof createLowlight>;
33-
type Root = ReturnType<Lowlight['highlight']>;
34-
35-
type LangSelectItem = {
36-
value: string;
37-
content: string;
38-
};
32+
export type {HighlightLangMap};
3933

4034
const pluginKey = new PluginKey<PluginState>('code_block_highlight');
4135

@@ -58,85 +52,34 @@ export type CodeBlockHighlightOptions = {
5852
};
5953

6054
export const CodeBlockHighlight: ExtensionAuto<CodeBlockHighlightOptions> = (builder, opts) => {
61-
let langs: NonNullable<HighlightLangMap>;
62-
let lowlight: Lowlight;
63-
let hljs: typeof HLJS;
64-
65-
const loadModules = async () => {
66-
try {
67-
hljs = (await import('highlight.js/lib/core')).default;
68-
const low = await import('lowlight');
69-
70-
const all: HighlightLangMap = low.all;
71-
const create: typeof createLowlight = low.createLowlight;
72-
langs = {...all, ...opts.langs};
73-
lowlight = create(langs);
74-
return true;
75-
} catch (e) {
76-
globalLogger.info('Skip code_block highlighting');
77-
builder.logger.log('Skip code_block highlighting');
78-
return false;
79-
}
80-
};
81-
8255
if (opts.lineWrapping?.enabled) builder.addPlugin(codeBlockLineWrappingPlugin);
8356
if (opts.lineNumbers?.enabled) builder.addPlugin(codeBlockLineNumbersPlugin);
8457

85-
builder.addPlugin(() => {
86-
let modulesLoaded = false;
87-
let view: EditorView | null = null;
88-
89-
// empty array by default, but is filled after loading modules
90-
const selectItems: LangSelectItem[] = [];
91-
const mapping: Record<string, string> = {};
58+
builder.addPlugin(() => codeBlockLangsPlugin(opts.langs, builder.logger));
9259

60+
builder.addPlugin(() => {
9361
// TODO: add TAB key handler
9462
// TODO: Remove constant selection of block
9563
return new Plugin<PluginState>({
9664
key: pluginKey,
9765
state: {
98-
init: (_, state) => {
99-
loadModules().then((loaded) => {
100-
modulesLoaded = loaded;
101-
102-
if (modulesLoaded) {
103-
for (const lang of Object.keys(langs)) {
104-
const defs = langs[lang](hljs);
105-
selectItems.push({
106-
value: lang,
107-
content: defs.name || capitalize(lang),
108-
});
109-
if (defs.aliases) {
110-
for (const alias of defs.aliases) {
111-
mapping[alias] = lang;
112-
}
113-
}
114-
}
115-
116-
selectItems.sort(sortLangs);
117-
118-
if (view && !view.isDestroyed) {
119-
view.dispatch(view.state.tr.setMeta(pluginKey, {modulesLoaded}));
120-
}
121-
}
122-
});
123-
66+
init: (_config, _state) => {
12467
const cache: HighlightCache = new WeakMap();
125-
126-
return {
127-
cache,
128-
decoSet: modulesLoaded
129-
? DecorationSet.empty
130-
: getDecorations(state.doc, cache),
131-
};
68+
return {cache, decoSet: DecorationSet.empty};
13269
},
133-
apply: (tr, {cache, decoSet}) => {
134-
if (!modulesLoaded) {
135-
return {cache, decoSet: DecorationSet.empty};
70+
apply: (tr, {cache, decoSet}, _oldState, newState) => {
71+
const langsUpdate = tr.getMeta(codeBlockLangsPluginKey);
72+
if (langsUpdate?.loaded && langsUpdate.lowlight) {
73+
return {
74+
cache,
75+
decoSet: getDecorations(tr.doc, cache, langsUpdate.lowlight),
76+
};
13677
}
13778

138-
if (tr.getMeta(pluginKey)?.modulesLoaded) {
139-
return {cache, decoSet: getDecorations(tr.doc, cache)};
79+
const {lowlight} = getCodeBlockLangsState(newState);
80+
81+
if (!lowlight) {
82+
return {cache, decoSet: DecorationSet.empty};
14083
}
14184

14285
if (!tr.docChanged) return {cache, decoSet};
@@ -167,9 +110,8 @@ export const CodeBlockHighlight: ExtensionAuto<CodeBlockHighlightOptions> = (bui
167110
return {cache, decoSet};
168111
},
169112
},
170-
view: (v) => {
171-
view = v;
172-
return codeLangSelectTooltipViewCreator(view, selectItems, mapping, {
113+
view: (view) => {
114+
return codeLangSelectTooltipViewCreator(view, {
173115
showCodeWrapping: Boolean(opts.lineWrapping?.enabled),
174116
showLineNumbers: Boolean(opts.lineNumbers?.enabled),
175117
});
@@ -185,11 +127,7 @@ export const CodeBlockHighlight: ExtensionAuto<CodeBlockHighlightOptions> = (bui
185127
});
186128
});
187129

188-
function getDecorations(doc: Node, cache: HighlightCache) {
189-
if (!lowlight) {
190-
return DecorationSet.empty;
191-
}
192-
130+
function getDecorations(doc: Node, cache: HighlightCache, lowlight: Lowlight) {
193131
const decos: Decoration[] = [];
194132

195133
for (const {node, pos} of findChildrenByType(doc, codeBlockType(doc.type.schema), true)) {
@@ -233,7 +171,7 @@ function renderTree(parsedNodes: HighlightParsedTree, from: number): Decoration[
233171
}
234172

235173
function parseNodes(
236-
nodes: Root['children'],
174+
nodes: LLRoot['children'],
237175
className: readonly string[] = [],
238176
): HighlightParsedTree {
239177
const result: HighlightParsedTree = [];
@@ -242,7 +180,7 @@ function parseNodes(
242180
}
243181

244182
function collectNodes(
245-
nodes: Root['children'],
183+
nodes: LLRoot['children'],
246184
className: readonly string[],
247185
result: HighlightParsedTree,
248186
): void {
@@ -258,10 +196,3 @@ function collectNodes(
258196
}
259197
}
260198
}
261-
262-
function sortLangs(a: LangSelectItem, b: LangSelectItem): number {
263-
// plaintext always goes first
264-
if (a.value === PlainTextLang) return -1;
265-
if (b.value === PlainTextLang) return 1;
266-
return 0;
267-
}

packages/editor/src/extensions/markdown/CodeBlock/CodeBlockHighlight/TooltipPlugin/index.tsx

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
1-
import type {SelectOption} from '@gravity-ui/uikit';
2-
31
import type {EditorView} from '#pm/view';
42
import {BaseTooltipPluginView} from 'src/plugins/BaseTooltip';
53

64
import {codeBlockType} from '../../CodeBlockSpecs';
5+
import {getCodeBlockLangsState} from '../plugins/codeBlockLangsPlugin';
76

87
import {CodeBlockToolbar} from './CodeBlockToolbar';
98

@@ -16,21 +15,21 @@ type Options = {
1615

1716
export const codeLangSelectTooltipViewCreator = (
1817
view: EditorView,
19-
langItems: SelectOption[],
20-
mapping: Record<string, string> = {},
2118
{showCodeWrapping, showLineNumbers}: Options,
2219
) => {
2320
return new BaseTooltipPluginView(view, {
2421
idPrefix: 'code-block-tooltip',
2522
nodeType: codeBlockType(view.state.schema),
2623
popupPlacement: ['bottom', 'top'],
2724
content: (view, {node, pos}, _onChange, _forceEdit, _onOutsideClick, rerender) => {
25+
const {langItems, aliasMapping} = getCodeBlockLangsState(view.state);
26+
2827
return (
2928
<CodeBlockToolbar
3029
pos={pos}
3130
node={node}
3231
editorView={view}
33-
mapping={mapping}
32+
mapping={aliasMapping}
3433
langItems={langItems}
3534
rerenderTooltip={rerender}
3635
showLineNumbers={showLineNumbers}
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import type {Options} from '@diplodoc/transform';
2+
import type HLJS from 'highlight.js/lib/core';
3+
import type {createLowlight} from 'lowlight' with {'resolution-mode': 'import'};
4+
5+
import type {EditorState} from '#pm/state';
6+
import {Plugin, PluginKey} from '#pm/state';
7+
import type {EditorView} from '#pm/view';
8+
import {capitalize} from 'src/lodash';
9+
import {globalLogger} from 'src/logger';
10+
11+
import {PlainTextLang} from '../const';
12+
13+
export type HighlightLangMap = Options['highlightLangs'];
14+
15+
export type Lowlight = ReturnType<typeof createLowlight>;
16+
export type LLRoot = ReturnType<Lowlight['highlight']>;
17+
18+
type LangItem = {
19+
value: string;
20+
content: string;
21+
};
22+
23+
type CodeBlockLangsState = {
24+
langItems: LangItem[];
25+
aliasMapping: Record<string, string>;
26+
lowlight: Lowlight | null;
27+
loaded: boolean;
28+
};
29+
30+
export const codeBlockLangsPluginKey = new PluginKey<CodeBlockLangsState>('code_block_langs');
31+
32+
const defaultState: CodeBlockLangsState = {
33+
langItems: [],
34+
aliasMapping: {},
35+
lowlight: null,
36+
loaded: false,
37+
};
38+
39+
export function getCodeBlockLangsState(state: EditorState): CodeBlockLangsState {
40+
return codeBlockLangsPluginKey.getState(state) ?? defaultState;
41+
}
42+
43+
export function codeBlockLangsPlugin(
44+
langsConfig: HighlightLangMap | undefined,
45+
logger: {log: (msg: string) => void},
46+
) {
47+
return new Plugin<CodeBlockLangsState>({
48+
key: codeBlockLangsPluginKey,
49+
state: {
50+
init: (_config, _state) => {
51+
return defaultState;
52+
},
53+
apply: (tr, state) => {
54+
const meta = tr.getMeta(codeBlockLangsPluginKey);
55+
if (meta) return meta as CodeBlockLangsState;
56+
return state;
57+
},
58+
},
59+
view: (view: EditorView) => {
60+
loadAndInit(view, langsConfig, logger);
61+
return {};
62+
},
63+
});
64+
}
65+
66+
async function loadAndInit(
67+
view: EditorView,
68+
langsConfig: HighlightLangMap | undefined,
69+
logger: {log: (msg: string) => void},
70+
) {
71+
try {
72+
const hljs: typeof HLJS = (await import('highlight.js/lib/core')).default;
73+
const low = await import('lowlight');
74+
75+
const all: HighlightLangMap = low.all;
76+
const create: typeof createLowlight = low.createLowlight;
77+
const langs: NonNullable<HighlightLangMap> = {...all, ...langsConfig};
78+
const lowlight = create(langs);
79+
80+
const langItems: LangItem[] = [];
81+
const aliasMapping: Record<string, string> = {};
82+
83+
for (const lang of Object.keys(langs)) {
84+
const defs = langs[lang](hljs);
85+
langItems.push({
86+
value: lang,
87+
content: defs.name || capitalize(lang),
88+
});
89+
if (defs.aliases) {
90+
for (const alias of defs.aliases) {
91+
aliasMapping[alias] = lang;
92+
}
93+
}
94+
}
95+
96+
langItems.sort(sortLangs);
97+
98+
if (!view.isDestroyed) {
99+
const newState: CodeBlockLangsState = {
100+
langItems,
101+
aliasMapping,
102+
lowlight,
103+
loaded: true,
104+
};
105+
view.dispatch(view.state.tr.setMeta(codeBlockLangsPluginKey, newState));
106+
}
107+
} catch (e) {
108+
globalLogger.info('Skip code_block highlighting');
109+
logger.log('Skip code_block highlighting');
110+
}
111+
}
112+
113+
function sortLangs(a: LangItem, b: LangItem): number {
114+
// plaintext always goes first
115+
if (a.value === PlainTextLang) return -1;
116+
if (b.value === PlainTextLang) return 1;
117+
return 0;
118+
}

0 commit comments

Comments
 (0)