288 lines
8.2 KiB
TypeScript
288 lines
8.2 KiB
TypeScript
/**
|
|
* Block 解析器
|
|
*/
|
|
|
|
import { EditorState } from '@codemirror/state';
|
|
import { syntaxTree, ensureSyntaxTree } from '@codemirror/language';
|
|
import type { Tree } from '@lezer/common';
|
|
import { Block as BlockNode, BlockDelimiter, BlockContent, BlockLanguage } from './lang-parser/parser.terms.js';
|
|
import {
|
|
SupportedLanguage,
|
|
DELIMITER_REGEX,
|
|
DELIMITER_PREFIX,
|
|
DELIMITER_SUFFIX,
|
|
AUTO_DETECT_SUFFIX,
|
|
Block
|
|
} from './types';
|
|
import { LANGUAGES } from './lang-parser/languages';
|
|
|
|
const DEFAULT_LANGUAGE = (LANGUAGES[0]?.token || 'text') as string;
|
|
|
|
/**
|
|
* 从语法树解析代码块
|
|
*/
|
|
export function getBlocksFromSyntaxTree(state: EditorState): Block[] | null {
|
|
const tree = syntaxTree(state);
|
|
if (!tree) {
|
|
return null;
|
|
}
|
|
return collectBlocksFromTree(tree, state);
|
|
}
|
|
|
|
function collectBlocksFromTree(tree: Tree, state: EditorState): Block[] | null {
|
|
const blocks: Block[] = [];
|
|
const doc = state.doc;
|
|
|
|
tree.iterate({
|
|
enter(node) {
|
|
if (node.type.id === BlockNode) {
|
|
let delimiter: { from: number; to: number } | null = null;
|
|
let content: { from: number; to: number } | null = null;
|
|
let language: string = DEFAULT_LANGUAGE;
|
|
let auto = false;
|
|
|
|
const blockNode = node.node;
|
|
blockNode.firstChild?.cursor().iterate(child => {
|
|
if (child.type.id === BlockDelimiter) {
|
|
delimiter = { from: child.from, to: child.to };
|
|
const delimiterText = doc.sliceString(child.from, child.to);
|
|
const match = delimiterText.match(/∞∞∞([a-zA-Z0-9_-]+)(-a)?\n/);
|
|
if (match) {
|
|
language = match[1] || DEFAULT_LANGUAGE;
|
|
auto = match[2] === '-a';
|
|
} else {
|
|
child.node.firstChild?.cursor().iterate(langChild => {
|
|
if (langChild.type.id === BlockLanguage) {
|
|
const langText = doc.sliceString(langChild.from, langChild.to);
|
|
language = langText || DEFAULT_LANGUAGE;
|
|
}
|
|
if (doc.sliceString(langChild.from, langChild.to) === AUTO_DETECT_SUFFIX) {
|
|
auto = true;
|
|
}
|
|
});
|
|
}
|
|
} else if (child.type.id === BlockContent) {
|
|
content = { from: child.from, to: child.to };
|
|
}
|
|
});
|
|
|
|
if (delimiter && content) {
|
|
blocks.push({
|
|
language: {
|
|
name: language as SupportedLanguage,
|
|
auto: auto,
|
|
},
|
|
content: content,
|
|
delimiter: delimiter,
|
|
range: {
|
|
from: node.from,
|
|
to: node.to,
|
|
},
|
|
});
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
if (blocks.length > 0) {
|
|
firstBlockDelimiterSize = blocks[0].delimiter.to;
|
|
return blocks;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
// 跟踪第一个分隔符的大小
|
|
export let firstBlockDelimiterSize: number | undefined;
|
|
|
|
/**
|
|
* 从文档字符串内容解析块,使用 String.indexOf()
|
|
*/
|
|
export function getBlocksFromString(state: EditorState): Block[] {
|
|
const blocks: Block[] = [];
|
|
const doc = state.doc;
|
|
|
|
if (doc.length === 0) {
|
|
return [createPlainTextBlock(0, 0)];
|
|
}
|
|
|
|
const content = doc.sliceString(0, doc.length);
|
|
const delimiter = DELIMITER_PREFIX;
|
|
const suffixLength = DELIMITER_SUFFIX.length;
|
|
|
|
let pos = content.indexOf(delimiter);
|
|
|
|
if (pos === -1) {
|
|
firstBlockDelimiterSize = 0;
|
|
return [createPlainTextBlock(0, doc.length)];
|
|
}
|
|
|
|
if (pos > 0) {
|
|
blocks.push(createPlainTextBlock(0, pos));
|
|
}
|
|
|
|
while (pos !== -1 && pos < doc.length) {
|
|
const blockStart = pos;
|
|
const langStart = blockStart + delimiter.length;
|
|
const delimiterEnd = content.indexOf(DELIMITER_SUFFIX, langStart);
|
|
if (delimiterEnd === -1) break;
|
|
|
|
const delimiterText = content.slice(blockStart, delimiterEnd + suffixLength);
|
|
const delimiterInfo = parseDelimiter(delimiterText);
|
|
if (!delimiterInfo) break;
|
|
|
|
const contentStart = delimiterEnd + suffixLength;
|
|
const nextDelimiter = content.indexOf(delimiter, contentStart);
|
|
const contentEnd = nextDelimiter === -1 ? doc.length : nextDelimiter;
|
|
|
|
blocks.push({
|
|
language: { name: delimiterInfo.language, auto: delimiterInfo.auto },
|
|
content: { from: contentStart, to: contentEnd },
|
|
delimiter: { from: blockStart, to: delimiterEnd + suffixLength },
|
|
range: { from: blockStart, to: contentEnd },
|
|
});
|
|
|
|
pos = nextDelimiter;
|
|
}
|
|
|
|
if (blocks.length === 0) {
|
|
blocks.push(createPlainTextBlock(0, doc.length));
|
|
firstBlockDelimiterSize = 0;
|
|
} else {
|
|
firstBlockDelimiterSize = blocks[0].delimiter.to;
|
|
}
|
|
|
|
return blocks;
|
|
}
|
|
|
|
/**
|
|
* 获取文档中的所有块
|
|
*/
|
|
export function getBlocks(state: EditorState): Block[] {
|
|
let blocks = getBlocksFromSyntaxTree(state);
|
|
if (blocks) {
|
|
return blocks;
|
|
}
|
|
|
|
const ensuredTree = ensureSyntaxTree(state, state.doc.length, 200);
|
|
if (ensuredTree) {
|
|
blocks = collectBlocksFromTree(ensuredTree, state);
|
|
if (blocks) {
|
|
return blocks;
|
|
}
|
|
}
|
|
|
|
return getBlocksFromString(state);
|
|
}
|
|
|
|
/**
|
|
* 获取当前光标所在的块
|
|
*/
|
|
export function getActiveBlock(state: EditorState): Block | undefined {
|
|
const range = state.selection.asSingle().ranges[0];
|
|
const blocks = getBlocks(state);
|
|
return blocks.find(block =>
|
|
block.range.from <= range.head && block.range.to >= range.head
|
|
);
|
|
}
|
|
|
|
/**
|
|
* 获取第一个块
|
|
*/
|
|
export function getFirstBlock(state: EditorState): Block | undefined {
|
|
const blocks = getBlocks(state);
|
|
return blocks[0];
|
|
}
|
|
|
|
/**
|
|
* 获取最后一个块
|
|
*/
|
|
export function getLastBlock(state: EditorState): Block | undefined {
|
|
const blocks = getBlocks(state);
|
|
return blocks[blocks.length - 1];
|
|
}
|
|
|
|
/**
|
|
* 根据位置获取块
|
|
*/
|
|
export function getBlockFromPos(state: EditorState, pos: number): Block | undefined {
|
|
const blocks = getBlocks(state);
|
|
return blocks.find(block =>
|
|
block.range.from <= pos && block.range.to >= pos
|
|
);
|
|
}
|
|
|
|
/**
|
|
* 获取块的行信息
|
|
*/
|
|
export function getBlockLineFromPos(state: EditorState, pos: number) {
|
|
const line = state.doc.lineAt(pos);
|
|
const block = getBlockFromPos(state, pos);
|
|
|
|
if (block) {
|
|
const firstBlockLine = state.doc.lineAt(block.content.from).number;
|
|
return {
|
|
line: line.number - firstBlockLine + 1,
|
|
col: pos - line.from,
|
|
length: line.length,
|
|
};
|
|
}
|
|
|
|
return {
|
|
line: line.number,
|
|
col: pos - line.from,
|
|
length: line.length,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* 创建新的分隔符文本
|
|
*/
|
|
export function createDelimiter(language: SupportedLanguage, autoDetect = false): string {
|
|
const suffix = autoDetect ? AUTO_DETECT_SUFFIX : '';
|
|
return `${DELIMITER_PREFIX}${language}${suffix}${DELIMITER_SUFFIX}`;
|
|
}
|
|
|
|
/**
|
|
* 验证分隔符格式
|
|
*/
|
|
export function isValidDelimiter(text: string): boolean {
|
|
DELIMITER_REGEX.lastIndex = 0;
|
|
return DELIMITER_REGEX.test(text);
|
|
}
|
|
|
|
/**
|
|
* 解析分隔符信息
|
|
*/
|
|
export function parseDelimiter(delimiterText: string): { language: SupportedLanguage; auto: boolean } | null {
|
|
DELIMITER_REGEX.lastIndex = 0;
|
|
const match = DELIMITER_REGEX.exec(delimiterText);
|
|
|
|
if (!match) {
|
|
return null;
|
|
}
|
|
|
|
const languageName = match[1];
|
|
const isAuto = match[2] === AUTO_DETECT_SUFFIX;
|
|
|
|
const validLanguage = LANGUAGES.some(lang => lang.token === languageName)
|
|
? languageName as SupportedLanguage
|
|
: DEFAULT_LANGUAGE as SupportedLanguage;
|
|
|
|
return {
|
|
language: validLanguage,
|
|
auto: isAuto
|
|
};
|
|
}
|
|
|
|
function createPlainTextBlock(from: number, to: number): Block {
|
|
return {
|
|
language: { name: DEFAULT_LANGUAGE, auto: false },
|
|
content: { from, to },
|
|
delimiter: { from: 0, to: 0 },
|
|
range: { from, to },
|
|
};
|
|
}
|
|
|
|
|