UNPKG

@atlaskit/editor-plugin-code-block

Version:

Code block plugin for @atlaskit/editor-core

187 lines (184 loc) 7.52 kB
import { getInsertedCodeBlocksInTransaction } from '@atlaskit/editor-common/code-block'; import { pmHistoryPluginKey } from '@atlaskit/editor-common/utils'; import { getAllChangedCodeBlocksInTransaction } from '../pm-plugins/utils'; const MIN_AUTO_DETECT_TEXT_LENGTH = 20; export const shouldTriggerLargeChangeDetection = (lastObservedText, text) => { if (!lastObservedText) { return text.length > 0; } return Math.abs(text.length - lastObservedText.length) > lastObservedText.length / 2; }; export const getFirstLine = text => { var _text$split$; return (_text$split$ = text.split('\n')[0]) !== null && _text$split$ !== void 0 ? _text$split$ : ''; }; export const hasEnoughTextForAutoDetection = text => text.trim().length >= MIN_AUTO_DETECT_TEXT_LENGTH; export const getLocalId = node => typeof node.attrs.localId === 'string' ? node.attrs.localId : null; export const createAutoDetectEntry = (node, pos, isPending, previous, options = {}) => ({ lastObservedText: node.textContent, lastObservedFirstLine: getFirstLine(node.textContent), isPending, detectionResult: options.preserveDetectionResult === false ? undefined : previous === null || previous === void 0 ? void 0 : previous.detectionResult, autoDetectedLanguage: previous === null || previous === void 0 ? void 0 : previous.autoDetectedLanguage, pos }); export const queueAutoDetection = (languageDetectionMap, node, pos, isPending) => { const localId = getLocalId(node); if (!localId) { return languageDetectionMap; } return { ...languageDetectionMap, [localId]: createAutoDetectEntry(node, pos, isPending, languageDetectionMap[localId]) }; }; export const removeAutoDetection = (languageDetectionMap, localId) => { if (!languageDetectionMap[localId]) { return languageDetectionMap; } const nextLanguageDetectionMap = { ...languageDetectionMap }; delete nextLanguageDetectionMap[localId]; return nextLanguageDetectionMap; }; const getCodeBlockLocalIdsRemovedFromChangedRanges = (tr, codeBlockType) => { const localIds = new Set(); tr.steps.forEach((step, stepIndex) => { const docAtStep = tr.docs[stepIndex]; step.getMap().forEach((oldStart, oldEnd) => { if (oldStart === oldEnd) { return; } const clampedOldEnd = Math.min(oldEnd, docAtStep.content.size); docAtStep.nodesBetween(oldStart, clampedOldEnd, (node, pos) => { if (node.type !== codeBlockType) { return true; } const isWholeCodeBlockRemoved = pos >= oldStart && pos + node.nodeSize <= clampedOldEnd; if (!isWholeCodeBlockRemoved) { return false; } const localId = getLocalId(node); if (localId) { localIds.add(localId); } return false; }); }); }); return localIds; }; const isHistoryMeta = meta => typeof meta === 'object' && meta !== null && 'redo' in meta; const getCodeBlockTransactionChanges = (tr, codeBlockType) => { const insertedNodesWithPos = getInsertedCodeBlocksInTransaction(tr, codeBlockType); const removedFromChangedRangesLocalIds = getCodeBlockLocalIdsRemovedFromChangedRanges(tr, codeBlockType); const insertedLocalIds = new Set(); const insertedCodeBlocks = []; insertedNodesWithPos.forEach(({ node, pos }) => { const localId = getLocalId(node); if (!localId) { return; } insertedLocalIds.add(localId); if (!removedFromChangedRangesLocalIds.has(localId)) { insertedCodeBlocks.push({ localId, node, pos }); } }); const deletedLocalIds = new Set(); removedFromChangedRangesLocalIds.forEach(localId => { if (!insertedLocalIds.has(localId)) { deletedLocalIds.add(localId); } }); return { deletedLocalIds, insertedCodeBlocks }; }; export const updateAutoDetectState = (tr, pluginState) => { const { codeBlock } = tr.doc.type.schema.nodes; const isPaste = tr.getMeta('paste') === true || tr.getMeta('uiEvent') === 'paste'; const isExternalContentChange = tr.getMeta('replaceDocument') === true || tr.getMeta('isRemote') === true; const historyMeta = tr.getMeta(pmHistoryPluginKey); const isUndo = isHistoryMeta(historyMeta) && historyMeta.redo === false; const hasTrackedEntries = Object.keys(pluginState.languageDetectionMap).length > 0; // Page loads and remote edits should not start auto-detection for code blocks. if (!hasTrackedEntries && isExternalContentChange) { return pluginState.languageDetectionMap; } // Existing entries still need mapping/deletion cleanup, but external edits should not refresh text. const changedCodeBlockNodes = isExternalContentChange ? [] : getAllChangedCodeBlocksInTransaction(tr); if (!hasTrackedEntries && !changedCodeBlockNodes.length) { return pluginState.languageDetectionMap; } const { deletedLocalIds, insertedCodeBlocks } = getCodeBlockTransactionChanges(tr, codeBlock); let languageDetectionMap = hasTrackedEntries ? Object.fromEntries(Object.entries(pluginState.languageDetectionMap).map(([localId, entry]) => [localId, { ...entry, pos: tr.mapping.map(entry.pos) }])) : pluginState.languageDetectionMap; if (!isExternalContentChange) { insertedCodeBlocks.forEach(({ localId, node, pos }) => { if (node.attrs.language) { languageDetectionMap = removeAutoDetection(languageDetectionMap, localId); return; } languageDetectionMap = queueAutoDetection(languageDetectionMap, node, pos, hasEnoughTextForAutoDetection(node.textContent)); }); changedCodeBlockNodes.forEach(({ node, pos }) => { var _tr$before$nodeAt; const localId = getLocalId(node); if (!localId || !languageDetectionMap[localId]) { return; } const currentLanguage = node.attrs.language; const previousEntry = languageDetectionMap[localId]; // Undo metadata does not identify the changed node; compare the pre/post language // so text undo inside auto-detection mode keeps the entry. const previousLanguage = isUndo ? (_tr$before$nodeAt = tr.before.nodeAt(tr.mapping.invert().map(pos))) === null || _tr$before$nodeAt === void 0 ? void 0 : _tr$before$nodeAt.attrs.language : undefined; if (isUndo && previousLanguage && !currentLanguage || currentLanguage && currentLanguage !== previousEntry.autoDetectedLanguage) { languageDetectionMap = removeAutoDetection(languageDetectionMap, localId); return; } const text = node.textContent; const firstLine = getFirstLine(text); const shouldTriggerDetection = previousEntry.isPending || isPaste || firstLine !== previousEntry.lastObservedFirstLine || shouldTriggerLargeChangeDetection(previousEntry.lastObservedText, text); const isPending = hasEnoughTextForAutoDetection(text) && shouldTriggerDetection; // Only pending detection refreshes the text snapshot; otherwise gradual typing // should continue comparing against the last attempted detection. languageDetectionMap = isPending ? queueAutoDetection(languageDetectionMap, node, pos, true) : { ...languageDetectionMap, [localId]: { ...previousEntry, isPending: false, pos } }; }); } deletedLocalIds.forEach(localId => { if (languageDetectionMap[localId]) { languageDetectionMap = removeAutoDetection(languageDetectionMap, localId); } }); return languageDetectionMap; };