@atlaskit/editor-plugin-code-block
Version:
Code block plugin for @atlaskit/editor-core
174 lines (161 loc) • 8.07 kB
JavaScript
/* eslint-disable require-unicode-regexp */
// Conservative weighted-regex heuristic for common high-confidence snippets, not a full classifier.
// Ambiguous snippets intentionally return null so users can select the language manually.
const MAX_DETECTION_CHARS = 10_000;
const MIN_DETECTION_SCORE = 3; // Require at least one medium-confidence signal before auto-selecting.
const MIN_SCORE_GAP = 2; // Avoid auto-selecting when top two languages are too close to distinguish.
// looksLikeJson regexes
const JSON_START_REGEX = /^[{[]/;
const JSON_END_REGEX = /[}\]]$/;
// html regexes
const HTML_DOCTYPE_REGEX = / {
// Reset stateful regexes so repeated tests always start at the beginning.
pattern.lastIndex = 0;
return pattern.test(code);
};
const scorePatterns = (code, patterns) => patterns.reduce((score, [pattern, value]) => score + (hasPattern(code, pattern) ? value : 0), 0);
const looksLikeHtmlTagPair = code => {
const openTags = new Set();
// Ignored via go/ees019
// eslint-disable-next-line e18e/prefer-static-regex
const tagPattern = /<\/?([a-z][a-z0-9-]*)\b[^<>]{0,500}>/gi;
let match;
while ((match = tagPattern.exec(code)) !== null) {
const [tag, tagName] = match;
if (tag.endsWith('/>')) {
continue;
}
if (tag.startsWith('</')) {
if (openTags.has(tagName.toLowerCase())) {
return true;
}
} else {
openTags.add(tagName.toLowerCase());
}
}
return false;
};
const looksLikeJson = code => {
const trimmed = code.trim();
if (!JSON_START_REGEX.test(trimmed) || !JSON_END_REGEX.test(trimmed)) {
return false;
}
try {
const parsed = JSON.parse(trimmed);
return parsed !== null && typeof parsed === 'object';
} catch {
return false;
}
};
const getLanguageScores = code => [{
language: 'json',
score: looksLikeJson(code) ? 8 : 0
}, {
language: 'html',
score: scorePatterns(code, [[HTML_DOCTYPE_REGEX, 5], [HTML_TAG_REGEX, 2]]) + (looksLikeHtmlTagPair(code) ? 3 : 0)
}, {
language: 'css',
score: scorePatterns(code, [[CSS_AT_RULE_REGEX, 3], [CSS_SELECTOR_PROPERTY_REGEX, 5], [CSS_PROPERTY_REGEX, 3]])
}, {
language: 'sql',
score: scorePatterns(code, [[SQL_SELECT_FROM_REGEX, 5], [SQL_DML_REGEX, 5], [SQL_CLAUSE_REGEX, 2]])
}, {
language: 'typescript',
score: scorePatterns(code, [[TS_INTERFACE_TYPE_REGEX, 5], [TS_IMPORT_EXPORT_TYPE_REGEX, 4], [TS_TYPED_VAR_REGEX, 4], [TS_RETURN_TYPE_REGEX, 3], [TS_ADVANCED_REGEX, 3], [TS_ACCESS_MODIFIER_REGEX, 5], [TS_TYPED_ARROW_PARAM_REGEX, 5]])
}, {
language: 'javascript',
score: scorePatterns(code, [[JS_VAR_REGEX, 3], [JS_FUNCTION_REGEX, 3], [JS_ARROW_REGEX, 3], [JS_IMPORT_EXPORT_REGEX, 3], [JS_COMMON_API_REGEX, 3]])
}, {
language: 'python',
score: scorePatterns(code, [[PY_DEF_REGEX, 5], [PY_CLASS_REGEX, 4], [PY_IMPORT_REGEX, 3], [PY_CONTROL_REGEX, 2], [PY_PRINT_REGEX, 2]])
}, {
language: 'java',
score: scorePatterns(code, [[JAVA_CLASS_REGEX, 5], [JAVA_MAIN_REGEX, 5], [JAVA_PRINTLN_REGEX, 4], [JAVA_MEMBER_REGEX, 3]])
}, {
language: 'go',
score: scorePatterns(code, [[GO_PACKAGE_REGEX, 5], [GO_FUNC_REGEX, 5], [GO_PRINTLN_REGEX, 3], [GO_IMPORT_REGEX, 2]])
}, {
language: 'ruby',
score: scorePatterns(code, [[RUBY_DEF_REGEX, 4], [RUBY_CLASS_REGEX, 3], [RUBY_END_REGEX, 3], [RUBY_COMMON_API_REGEX, 3], [RUBY_BLOCK_REGEX, 2]])
}, {
language: 'rust',
score: scorePatterns(code, [[RUST_FN_REGEX, 5], [RUST_LET_MUT_REGEX, 3], [RUST_PRINTLN_REGEX, 4], [RUST_USE_REGEX, 3], [RUST_STRUCT_REGEX, 3]])
}, {
language: 'shell',
score: scorePatterns(code, [[SHELL_SHEBANG_REGEX, 6], [SHELL_CONTROL_REGEX, 4], [SHELL_FI_DONE_REGEX, 3], [SHELL_COMMAND_REGEX, 3], [SHELL_VAR_REGEX, 2]])
}];
export const detectLanguage = code => {
const trimmedCode = code.trim();
if (trimmedCode.length < 8) {
return null;
}
const codeForDetection = trimmedCode.slice(0, MAX_DETECTION_CHARS);
const [best, secondBest] = getLanguageScores(codeForDetection).filter(({
score
}) => score > 0).sort((a, b) => b.score - a.score);
if (!best || best.score < MIN_DETECTION_SCORE) {
return null;
}
if (secondBest && best.score - secondBest.score < MIN_SCORE_GAP) {
return null;
}
return best.language;
};