Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/editor/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
"test:cov": "jest --coverage",
"test:watch": "jest --watchAll",
"test:esbuild": "node tests/esbuild-test/esbuild-tester.js",
"test:circular-deps": "node scripts/check-circular-deps.js 48",
"test:circular-deps": "node scripts/check-circular-deps.js 47",
"prepack": "cp ../../README.md ./README.md",
"postpack": "rm -f ./README.md",
"prepublishOnly": "pnpm run lint && pnpm run clean && pnpm run build"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,11 @@
import type {Options} from '@diplodoc/transform';
// importing only type, because lowlight and highlight.js is optional deps
import type HLJS from 'highlight.js/lib/core';
import type {createLowlight} from 'lowlight' with {'resolution-mode': 'import'};
import type {Node} from 'prosemirror-model';
import {Plugin, PluginKey} from 'prosemirror-state';
// @ts-ignore // TODO: fix cjs build
import {findChildrenByType} from 'prosemirror-utils';
import {Decoration, DecorationSet, type EditorView} from 'prosemirror-view';
import {Decoration, DecorationSet} from 'prosemirror-view';

import type {ExtensionAuto} from '#core';

import type {ExtensionAuto} from '../../../../core';
import {capitalize} from '../../../../lodash';
import {globalLogger} from '../../../../logger';
import {
CodeBlockNodeAttr,
type LineNumbersOptions,
Expand All @@ -20,22 +15,21 @@ import {

import {CodeBlockNodeView} from './CodeBlockNodeView';
import {codeLangSelectTooltipViewCreator} from './TooltipPlugin';
import {PlainTextLang} from './const';
import {
type HighlightLangMap,
type LLRoot,
type Lowlight,
codeBlockLangsPlugin,
codeBlockLangsPluginKey,
getCodeBlockLangsState,
} from './plugins/codeBlockLangsPlugin';
import {codeBlockLineNumbersPlugin} from './plugins/codeBlockLineNumbersPlugin';
import {codeBlockLineWrappingPlugin} from './plugins/codeBlockLineWrappingPlugin';
import {processChangedCodeBlocks} from './utils';

import './CodeBlockHighlight.scss';

export type HighlightLangMap = Options['highlightLangs'];

type Lowlight = ReturnType<typeof createLowlight>;
type Root = ReturnType<Lowlight['highlight']>;

type LangSelectItem = {
value: string;
content: string;
};
export type {HighlightLangMap};

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

Expand All @@ -58,85 +52,34 @@ export type CodeBlockHighlightOptions = {
};

export const CodeBlockHighlight: ExtensionAuto<CodeBlockHighlightOptions> = (builder, opts) => {
let langs: NonNullable<HighlightLangMap>;
let lowlight: Lowlight;
let hljs: typeof HLJS;

const loadModules = async () => {
try {
hljs = (await import('highlight.js/lib/core')).default;
const low = await import('lowlight');

const all: HighlightLangMap = low.all;
const create: typeof createLowlight = low.createLowlight;
langs = {...all, ...opts.langs};
lowlight = create(langs);
return true;
} catch (e) {
globalLogger.info('Skip code_block highlighting');
builder.logger.log('Skip code_block highlighting');
return false;
}
};

if (opts.lineWrapping?.enabled) builder.addPlugin(codeBlockLineWrappingPlugin);
if (opts.lineNumbers?.enabled) builder.addPlugin(codeBlockLineNumbersPlugin);

builder.addPlugin(() => {
let modulesLoaded = false;
let view: EditorView | null = null;

// empty array by default, but is filled after loading modules
const selectItems: LangSelectItem[] = [];
const mapping: Record<string, string> = {};
builder.addPlugin(() => codeBlockLangsPlugin(opts.langs, builder.logger));

builder.addPlugin(() => {
// TODO: add TAB key handler
// TODO: Remove constant selection of block
return new Plugin<PluginState>({
key: pluginKey,
state: {
init: (_, state) => {
loadModules().then((loaded) => {
modulesLoaded = loaded;

if (modulesLoaded) {
for (const lang of Object.keys(langs)) {
const defs = langs[lang](hljs);
selectItems.push({
value: lang,
content: defs.name || capitalize(lang),
});
if (defs.aliases) {
for (const alias of defs.aliases) {
mapping[alias] = lang;
}
}
}

selectItems.sort(sortLangs);

if (view && !view.isDestroyed) {
view.dispatch(view.state.tr.setMeta(pluginKey, {modulesLoaded}));
}
}
});

init: (_config, _state) => {
const cache: HighlightCache = new WeakMap();

return {
cache,
decoSet: modulesLoaded
? DecorationSet.empty
: getDecorations(state.doc, cache),
};
return {cache, decoSet: DecorationSet.empty};
},
apply: (tr, {cache, decoSet}) => {
if (!modulesLoaded) {
return {cache, decoSet: DecorationSet.empty};
apply: (tr, {cache, decoSet}, _oldState, newState) => {
const langsUpdate = tr.getMeta(codeBlockLangsPluginKey);
if (langsUpdate?.loaded && langsUpdate.lowlight) {
return {
cache,
decoSet: getDecorations(tr.doc, cache, langsUpdate.lowlight),
};
}

if (tr.getMeta(pluginKey)?.modulesLoaded) {
return {cache, decoSet: getDecorations(tr.doc, cache)};
const {lowlight} = getCodeBlockLangsState(newState);

if (!lowlight) {
return {cache, decoSet: DecorationSet.empty};
}

if (!tr.docChanged) return {cache, decoSet};
Expand Down Expand Up @@ -167,9 +110,8 @@ export const CodeBlockHighlight: ExtensionAuto<CodeBlockHighlightOptions> = (bui
return {cache, decoSet};
},
},
view: (v) => {
view = v;
return codeLangSelectTooltipViewCreator(view, selectItems, mapping, {
view: (view) => {
return codeLangSelectTooltipViewCreator(view, {
showCodeWrapping: Boolean(opts.lineWrapping?.enabled),
showLineNumbers: Boolean(opts.lineNumbers?.enabled),
});
Expand All @@ -185,11 +127,7 @@ export const CodeBlockHighlight: ExtensionAuto<CodeBlockHighlightOptions> = (bui
});
});

function getDecorations(doc: Node, cache: HighlightCache) {
if (!lowlight) {
return DecorationSet.empty;
}

function getDecorations(doc: Node, cache: HighlightCache, lowlight: Lowlight) {
const decos: Decoration[] = [];

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

function parseNodes(
nodes: Root['children'],
nodes: LLRoot['children'],
className: readonly string[] = [],
): HighlightParsedTree {
const result: HighlightParsedTree = [];
Expand All @@ -242,7 +180,7 @@ function parseNodes(
}

function collectNodes(
nodes: Root['children'],
nodes: LLRoot['children'],
className: readonly string[],
result: HighlightParsedTree,
): void {
Expand All @@ -258,10 +196,3 @@ function collectNodes(
}
}
}

function sortLangs(a: LangSelectItem, b: LangSelectItem): number {
// plaintext always goes first
if (a.value === PlainTextLang) return -1;
if (b.value === PlainTextLang) return 1;
return 0;
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import type {SelectOption} from '@gravity-ui/uikit';

import type {EditorView} from '#pm/view';
import {BaseTooltipPluginView} from 'src/plugins/BaseTooltip';

import {codeBlockType} from '../../CodeBlockSpecs';
import {getCodeBlockLangsState} from '../plugins/codeBlockLangsPlugin';

import {CodeBlockToolbar} from './CodeBlockToolbar';

Expand All @@ -16,21 +15,21 @@ type Options = {

export const codeLangSelectTooltipViewCreator = (
view: EditorView,
langItems: SelectOption[],
mapping: Record<string, string> = {},
{showCodeWrapping, showLineNumbers}: Options,
) => {
return new BaseTooltipPluginView(view, {
idPrefix: 'code-block-tooltip',
nodeType: codeBlockType(view.state.schema),
popupPlacement: ['bottom', 'top'],
content: (view, {node, pos}, _onChange, _forceEdit, _onOutsideClick, rerender) => {
const {langItems, aliasMapping} = getCodeBlockLangsState(view.state);

return (
<CodeBlockToolbar
pos={pos}
node={node}
editorView={view}
mapping={mapping}
mapping={aliasMapping}
langItems={langItems}
rerenderTooltip={rerender}
showLineNumbers={showLineNumbers}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import type {Options} from '@diplodoc/transform';
import type {createLowlight} from 'lowlight' with {'resolution-mode': 'import'};

import type {EditorState} from '#pm/state';
import {Plugin, PluginKey} from '#pm/state';
import type {EditorView} from '#pm/view';
import {capitalize} from 'src/lodash';
import {globalLogger} from 'src/logger';

import {PlainTextLang} from '../const';

export type HighlightLangMap = Options['highlightLangs'];

export type Lowlight = ReturnType<typeof createLowlight>;
export type LLRoot = ReturnType<Lowlight['highlight']>;

type LangItem = {
value: string;
content: string;
};

type CodeBlockLangsState = {
langItems: LangItem[];
aliasMapping: Record<string, string>;
lowlight: Lowlight | null;
loaded: boolean;
};

export const codeBlockLangsPluginKey = new PluginKey<CodeBlockLangsState>('code_block_langs');

const defaultState: CodeBlockLangsState = {
langItems: [],
aliasMapping: {},
lowlight: null,
loaded: false,
};

export function getCodeBlockLangsState(state: EditorState): CodeBlockLangsState {
return codeBlockLangsPluginKey.getState(state) ?? defaultState;
}

export function codeBlockLangsPlugin(
langsConfig: HighlightLangMap | undefined,
logger: {log: (msg: string) => void},
) {
return new Plugin<CodeBlockLangsState>({
key: codeBlockLangsPluginKey,
state: {
init: (_config, _state) => {
return defaultState;
},
apply: (tr, state) => {
const meta = tr.getMeta(codeBlockLangsPluginKey);
if (meta) return meta as CodeBlockLangsState;
return state;
},
},
view: (view: EditorView) => {
loadAndInit(view, langsConfig, logger);
return {};
},
});
}

async function loadAndInit(
view: EditorView,
langsConfig: HighlightLangMap | undefined,
logger: {log: (msg: string) => void},
) {
try {
const [{default: hljs}, low] = await Promise.all([
import('highlight.js/lib/core'),
import('lowlight'),
]);

const all: HighlightLangMap = low.all;
const create: typeof createLowlight = low.createLowlight;
const langs: NonNullable<HighlightLangMap> = {...all, ...langsConfig};
const lowlight = create(langs);

const langItems: LangItem[] = [];
const aliasMapping: Record<string, string> = {};

for (const lang of Object.keys(langs)) {
const defs = langs[lang](hljs);
langItems.push({
value: lang,
content: defs.name || capitalize(lang),
});
if (defs.aliases) {
for (const alias of defs.aliases) {
aliasMapping[alias] = lang;
}
}
}

langItems.sort(sortLangs);

if (!view.isDestroyed) {
const newState: CodeBlockLangsState = {
langItems,
aliasMapping,
lowlight,
loaded: true,
};
view.dispatch(view.state.tr.setMeta(codeBlockLangsPluginKey, newState));
}
} catch (e) {
globalLogger.info('Skip code_block highlighting');
logger.log('Skip code_block highlighting');
}
}

function sortLangs(a: LangItem, b: LangItem): number {
// plaintext always goes first
if (a.value === PlainTextLang) return -1;
if (b.value === PlainTextLang) return 1;
return 0;
}
Loading