675 lines
17 KiB
TypeScript
675 lines
17 KiB
TypeScript
import { jsonrepair } from "jsonrepair";
|
|
import Prism from "prismjs";
|
|
import "prismjs/components/prism-json";
|
|
import "prismjs/components/prism-javascript";
|
|
import "prismjs/components/prism-typescript";
|
|
import "prismjs/components/prism-jsx";
|
|
import "prismjs/components/prism-tsx";
|
|
import "prismjs/components/prism-python";
|
|
import "prismjs/components/prism-bash";
|
|
import "prismjs/components/prism-yaml";
|
|
import "prismjs/components/prism-markdown";
|
|
|
|
const DEFAULT_CODE_CHAR_WIDTH_RATIO = 0.62;
|
|
const DEFAULT_CODE_LINE_HEIGHT_RATIO = 1.25;
|
|
const DEFAULT_FONT_STEP = 0.5;
|
|
const HARD_MIN_FONT_SIZE = 4;
|
|
|
|
export const DEFAULT_CODE_FONT_FAMILY = "var(--code-font-family,'Liberation Mono', monospace)";
|
|
export const PRISM_CODE_BLOCK_STYLES = `
|
|
.prism-code-block .token {
|
|
display: inline !important;
|
|
white-space: inherit !important;
|
|
}
|
|
|
|
.prism-code-block {
|
|
--code-fg: var(--background-text, #dbe5ff);
|
|
--code-accent: var(--primary-color, #7aa2ff);
|
|
color: var(--code-fg);
|
|
}
|
|
|
|
.prism-code-block .token.comment,
|
|
.prism-code-block .token.prolog,
|
|
.prism-code-block .token.doctype,
|
|
.prism-code-block .token.cdata {
|
|
color: var(--code-fg);
|
|
opacity: 0.62;
|
|
}
|
|
|
|
.prism-code-block .token.punctuation {
|
|
color: var(--code-fg);
|
|
opacity: 0.82;
|
|
}
|
|
|
|
.prism-code-block .token.property,
|
|
.prism-code-block .token.tag,
|
|
.prism-code-block .token.constant,
|
|
.prism-code-block .token.symbol,
|
|
.prism-code-block .token.deleted {
|
|
color: var(--graph-0, #7bc4ff);
|
|
color: color-mix(in srgb, var(--graph-0, #7bc4ff) 72%, var(--code-fg) 28%);
|
|
}
|
|
|
|
.prism-code-block .token.boolean,
|
|
.prism-code-block .token.number {
|
|
color: var(--graph-1, #f5c97b);
|
|
color: color-mix(in srgb, var(--graph-1, #f5c97b) 68%, var(--code-fg) 32%);
|
|
}
|
|
|
|
.prism-code-block .token.selector,
|
|
.prism-code-block .token.attr-name,
|
|
.prism-code-block .token.string,
|
|
.prism-code-block .token.char,
|
|
.prism-code-block .token.builtin,
|
|
.prism-code-block .token.inserted {
|
|
color: var(--graph-2, #9fe6b8);
|
|
color: color-mix(in srgb, var(--graph-2, #9fe6b8) 72%, var(--code-fg) 28%);
|
|
}
|
|
|
|
.prism-code-block .token.operator,
|
|
.prism-code-block .token.entity,
|
|
.prism-code-block .token.url,
|
|
.prism-code-block .token.variable {
|
|
color: var(--graph-3, #f5a97f);
|
|
color: color-mix(in srgb, var(--graph-3, #f5a97f) 70%, var(--code-fg) 30%);
|
|
}
|
|
|
|
.prism-code-block .token.atrule,
|
|
.prism-code-block .token.attr-value,
|
|
.prism-code-block .token.function,
|
|
.prism-code-block .token.class-name {
|
|
color: var(--graph-4, #b8a8ff);
|
|
color: color-mix(in srgb, var(--graph-4, #b8a8ff) 74%, var(--code-fg) 26%);
|
|
}
|
|
|
|
.prism-code-block .token.keyword {
|
|
color: var(--code-accent);
|
|
color: color-mix(in srgb, var(--code-accent, #7aa2ff) 78%, var(--code-fg) 22%);
|
|
font-weight: 600;
|
|
}
|
|
|
|
.prism-code-block .token.regex,
|
|
.prism-code-block .token.important {
|
|
color: var(--code-accent);
|
|
color: color-mix(in srgb, var(--code-accent, #7aa2ff) 64%, var(--graph-1, #f5c97b) 36%);
|
|
}
|
|
`;
|
|
|
|
interface FitCodeBlockOptions {
|
|
language?: string;
|
|
content?: string;
|
|
maxWidth: number;
|
|
maxHeight: number;
|
|
maxFontSize?: number;
|
|
minFontSize?: number;
|
|
fontStep?: number;
|
|
charWidthRatio?: number;
|
|
lineHeightRatio?: number;
|
|
}
|
|
|
|
interface TypographyCandidate {
|
|
lineHeight: number;
|
|
maxCharsPerLine: number;
|
|
renderedLineCount: number;
|
|
}
|
|
|
|
export interface FittedCodeBlock {
|
|
text: string;
|
|
highlightedHtml: string;
|
|
prismLanguage: string;
|
|
fontSize: number;
|
|
lineHeight: number;
|
|
fontFamily: string;
|
|
}
|
|
|
|
function splitCollapsedPythonImports(line: string) {
|
|
const importSegments = line
|
|
.split(/(?=\sfrom\s+[A-Za-z0-9_.]+\s+import\s+)/g)
|
|
.map((segment) => segment.trim())
|
|
.filter(Boolean);
|
|
|
|
return importSegments.length > 1 ? importSegments : [line];
|
|
}
|
|
|
|
function expandInlinePythonStatement(line: string) {
|
|
const inlineReturnMatch = line.match(/^(\s*def\s+[^(]+\([^)]*\):)\s+return\s+(.+)$/);
|
|
|
|
if (!inlineReturnMatch) {
|
|
return [line];
|
|
}
|
|
|
|
return [inlineReturnMatch[1], `return ${inlineReturnMatch[2]}`];
|
|
}
|
|
|
|
function expandPathListAssignment(line: string) {
|
|
const trimmedLine = line.trim();
|
|
|
|
if (!trimmedLine.startsWith("urlpatterns = [") || !trimmedLine.endsWith("]")) {
|
|
return [line];
|
|
}
|
|
|
|
const pathCalls = trimmedLine.match(/path\([^)]*\)/g);
|
|
|
|
if (!pathCalls?.length) {
|
|
return [line];
|
|
}
|
|
|
|
return [
|
|
"urlpatterns = [",
|
|
...pathCalls.map((pathCall) => ` ${pathCall},`),
|
|
"]",
|
|
];
|
|
}
|
|
|
|
function normalizePythonCode(content: string) {
|
|
const normalizedLines: string[] = [];
|
|
|
|
for (const line of content.split("\n")) {
|
|
const importLines = splitCollapsedPythonImports(line);
|
|
|
|
for (const importLine of importLines) {
|
|
const expandedPathLines = expandPathListAssignment(importLine);
|
|
|
|
for (const expandedPathLine of expandedPathLines) {
|
|
normalizedLines.push(...expandInlinePythonStatement(expandedPathLine));
|
|
}
|
|
}
|
|
}
|
|
|
|
return normalizedLines.join("\n").replace(/\n{3,}/g, "\n\n");
|
|
}
|
|
|
|
function tryFormatJson(content: string) {
|
|
const trimmedContent = content.replace(/^\uFEFF/, "").trim();
|
|
|
|
if (!trimmedContent) {
|
|
return "";
|
|
}
|
|
|
|
const normalizedSeparatorsContent = trimmedContent
|
|
.replace(/^\s*\/\s*$/gm, ",")
|
|
.replace(/\r\n?/g, "\n")
|
|
.replace(/\n\s*:\s*/g, ": ")
|
|
.replace(/\n\s*,\s*/g, ", ");
|
|
|
|
const parseAndFormat = (raw: string) => {
|
|
try {
|
|
const parsed = JSON.parse(raw);
|
|
|
|
if (typeof parsed === "string") {
|
|
try {
|
|
return JSON.stringify(JSON.parse(parsed), null, 2);
|
|
} catch {
|
|
return JSON.stringify(parsed, null, 2);
|
|
}
|
|
}
|
|
|
|
return JSON.stringify(parsed, null, 2);
|
|
} catch {
|
|
return null;
|
|
}
|
|
};
|
|
|
|
const extractedJsonMatch = normalizedSeparatorsContent.match(/[\[{][\s\S]*[\]}]/);
|
|
const extractedJsonCandidate = extractedJsonMatch?.[0];
|
|
|
|
const candidates = [
|
|
normalizedSeparatorsContent,
|
|
trimmedContent,
|
|
extractedJsonCandidate,
|
|
].filter((candidate): candidate is string => Boolean(candidate));
|
|
|
|
for (const candidate of candidates) {
|
|
const direct = parseAndFormat(candidate);
|
|
if (direct !== null) {
|
|
return direct;
|
|
}
|
|
|
|
try {
|
|
const repairedJson = jsonrepair(candidate);
|
|
const repaired = parseAndFormat(repairedJson);
|
|
if (repaired !== null) {
|
|
return repaired;
|
|
}
|
|
} catch {
|
|
// Try next parsing strategy.
|
|
}
|
|
}
|
|
|
|
const jsonLikeTokenMatch = normalizedSeparatorsContent.match(
|
|
/"(?:\\.|[^"\\])*"|true|false|null|-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?|[{}\[\]:,\/]/g
|
|
);
|
|
|
|
if (jsonLikeTokenMatch?.length) {
|
|
const normalizedTokens = jsonLikeTokenMatch.map((token) => (token === "/" ? "," : token));
|
|
let rebuilt = "";
|
|
|
|
for (const token of normalizedTokens) {
|
|
if (token === ":" || token === ",") {
|
|
rebuilt = rebuilt.replace(/\s*$/, "");
|
|
rebuilt += `${token} `;
|
|
continue;
|
|
}
|
|
|
|
if (token === "}" || token === "]") {
|
|
rebuilt = rebuilt.replace(/\s*$/, "");
|
|
rebuilt += token;
|
|
continue;
|
|
}
|
|
|
|
if (token === "{" || token === "[") {
|
|
rebuilt = rebuilt.replace(/\s*$/, "");
|
|
rebuilt += token;
|
|
continue;
|
|
}
|
|
|
|
rebuilt += token;
|
|
}
|
|
|
|
const rebuiltDirect = parseAndFormat(rebuilt);
|
|
if (rebuiltDirect !== null) {
|
|
return rebuiltDirect;
|
|
}
|
|
|
|
try {
|
|
const rebuiltRepaired = parseAndFormat(jsonrepair(rebuilt));
|
|
if (rebuiltRepaired !== null) {
|
|
return rebuiltRepaired;
|
|
}
|
|
} catch {
|
|
// Continue to best-effort line merge fallback below.
|
|
}
|
|
}
|
|
|
|
const normalizedLines = normalizedSeparatorsContent
|
|
.replace(/\n\s*:\s*\n\s*/g, ": ")
|
|
.replace(/\n\s*\/\s*\n/g, ",\n")
|
|
.split("\n")
|
|
.map((line) => line.replace(/\s+$/g, ""));
|
|
|
|
const mergedLines: string[] = [];
|
|
|
|
for (const rawLine of normalizedLines) {
|
|
const trimmedLine = rawLine.trim();
|
|
|
|
if (!trimmedLine) {
|
|
continue;
|
|
}
|
|
|
|
if (trimmedLine === ":") {
|
|
if (mergedLines.length > 0) {
|
|
mergedLines[mergedLines.length - 1] = `${mergedLines[mergedLines.length - 1]}:`;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if (trimmedLine === "/") {
|
|
if (mergedLines.length > 0 && !mergedLines[mergedLines.length - 1].trim().endsWith(",")) {
|
|
mergedLines[mergedLines.length - 1] = `${mergedLines[mergedLines.length - 1]},`;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
const previousLine = mergedLines[mergedLines.length - 1]?.trim() || "";
|
|
if (previousLine.endsWith(":")) {
|
|
mergedLines[mergedLines.length - 1] = `${mergedLines[mergedLines.length - 1]} ${trimmedLine}`;
|
|
continue;
|
|
}
|
|
|
|
mergedLines.push(rawLine);
|
|
}
|
|
|
|
for (let index = 0; index < mergedLines.length - 1; index += 1) {
|
|
const currentLine = mergedLines[index].trim();
|
|
const nextLine = mergedLines[index + 1].trim();
|
|
const currentEndsWithComma = currentLine.endsWith(",");
|
|
const currentIsContainerStart = currentLine.endsWith("{") || currentLine.endsWith("[");
|
|
const nextStartsNewKey = nextLine.startsWith("\"");
|
|
const nextIsContainerEnd = nextLine.startsWith("}") || nextLine.startsWith("]");
|
|
|
|
if (!currentEndsWithComma && !currentIsContainerStart && nextStartsNewKey && !nextIsContainerEnd) {
|
|
mergedLines[index] = `${mergedLines[index]},`;
|
|
}
|
|
}
|
|
|
|
return mergedLines
|
|
.join("\n")
|
|
.replace(/,\s*([}\]])/g, "$1");
|
|
}
|
|
|
|
function isValidJsonContent(content: string) {
|
|
try {
|
|
JSON.parse(content.trim());
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function seemsJsonLike(content: string) {
|
|
const trimmed = content.trim();
|
|
if (!trimmed) {
|
|
return false;
|
|
}
|
|
|
|
if (/^[\[{]/.test(trimmed)) {
|
|
return true;
|
|
}
|
|
|
|
return /"[^"\n]+"\s*:/.test(trimmed) || /[\[{][\s\S]*[\]}]/.test(trimmed);
|
|
}
|
|
|
|
function unwrapMarkdownCodeFence(content: string) {
|
|
const trimmedContent = content.trim();
|
|
const fencedCodeMatch = trimmedContent.match(/^```([^\n`]*)\n([\s\S]*?)\n```$/);
|
|
|
|
if (!fencedCodeMatch) {
|
|
return {
|
|
content: content,
|
|
fenceLanguage: undefined as string | undefined,
|
|
};
|
|
}
|
|
|
|
return {
|
|
content: fencedCodeMatch[2],
|
|
fenceLanguage: fencedCodeMatch[1]?.trim().toLowerCase() || undefined,
|
|
};
|
|
}
|
|
|
|
function escapeHtml(content: string) {
|
|
return content
|
|
.replace(/&/g, "&")
|
|
.replace(/</g, "<")
|
|
.replace(/>/g, ">");
|
|
}
|
|
|
|
function resolvePrismLanguage(language?: string) {
|
|
const normalizedLanguage = language?.toLowerCase().trim();
|
|
|
|
if (!normalizedLanguage) {
|
|
return "clike";
|
|
}
|
|
|
|
if (normalizedLanguage.includes("json")) {
|
|
return "json";
|
|
}
|
|
|
|
if (normalizedLanguage.includes("python")) {
|
|
return "python";
|
|
}
|
|
|
|
if (normalizedLanguage === "ts") {
|
|
return "typescript";
|
|
}
|
|
|
|
if (normalizedLanguage === "js") {
|
|
return "javascript";
|
|
}
|
|
|
|
if (normalizedLanguage === "py") {
|
|
return "python";
|
|
}
|
|
|
|
if (normalizedLanguage === "sh" || normalizedLanguage === "shell") {
|
|
return "bash";
|
|
}
|
|
|
|
if (normalizedLanguage === "yml") {
|
|
return "yaml";
|
|
}
|
|
|
|
if (Prism.languages[normalizedLanguage]) {
|
|
return normalizedLanguage;
|
|
}
|
|
|
|
return "clike";
|
|
}
|
|
|
|
function highlightCode(content: string, language?: string) {
|
|
const prismLanguage = resolvePrismLanguage(language);
|
|
const grammar = Prism.languages[prismLanguage];
|
|
|
|
if (!grammar) {
|
|
return {
|
|
html: escapeHtml(content),
|
|
prismLanguage,
|
|
};
|
|
}
|
|
|
|
try {
|
|
return {
|
|
html: Prism.highlight(content, grammar, prismLanguage),
|
|
prismLanguage,
|
|
};
|
|
} catch {
|
|
return {
|
|
html: escapeHtml(content),
|
|
prismLanguage,
|
|
};
|
|
}
|
|
}
|
|
|
|
export function normalizeCodeContent(language?: string, content?: string) {
|
|
let normalizedContent = (content || "")
|
|
.replace(/\r\n?/g, "\n")
|
|
.replace(/\\\[/g, "[")
|
|
.replace(/\\\]/g, "]");
|
|
const unwrappedContent = unwrapMarkdownCodeFence(normalizedContent);
|
|
normalizedContent = unwrappedContent.content.trimEnd();
|
|
|
|
const normalizedLanguage = language?.toLowerCase()?.trim() || unwrappedContent.fenceLanguage;
|
|
const isJsonLanguage = normalizedLanguage?.includes("json");
|
|
const looksLikeJsonPayload = seemsJsonLike(normalizedContent);
|
|
|
|
if (normalizedLanguage === "python") {
|
|
normalizedContent = normalizePythonCode(normalizedContent);
|
|
} else if (isJsonLanguage || looksLikeJsonPayload) {
|
|
const formattedJson = tryFormatJson(normalizedContent);
|
|
normalizedContent = formattedJson;
|
|
}
|
|
|
|
return normalizedContent;
|
|
}
|
|
|
|
function countRenderedLines(content: string, maxCharsPerLine: number) {
|
|
const rawLines = content.split("\n");
|
|
let renderedLineCount = 0;
|
|
|
|
for (const rawLine of rawLines) {
|
|
const expandedLine = rawLine.replace(/\t/g, " ");
|
|
|
|
if (expandedLine.length === 0) {
|
|
renderedLineCount += 1;
|
|
continue;
|
|
}
|
|
|
|
renderedLineCount += Math.max(1, Math.ceil(expandedLine.length / maxCharsPerLine));
|
|
}
|
|
|
|
return Math.max(1, renderedLineCount);
|
|
}
|
|
|
|
function splitLineForLineBudget(line: string, maxCharsPerLine: number) {
|
|
if (line.length === 0) {
|
|
return [""];
|
|
}
|
|
|
|
const chunks: string[] = [];
|
|
|
|
for (let start = 0; start < line.length; start += maxCharsPerLine) {
|
|
chunks.push(line.slice(start, start + maxCharsPerLine));
|
|
}
|
|
|
|
return chunks;
|
|
}
|
|
|
|
function truncateContentToLineBudget(
|
|
content: string,
|
|
lineBudget: number,
|
|
maxCharsPerLine: number
|
|
) {
|
|
const linesForDisplay: string[] = [];
|
|
const rawLines = content.split("\n");
|
|
|
|
for (const rawLine of rawLines) {
|
|
const expandedLine = rawLine.replace(/\t/g, " ");
|
|
const chunks = splitLineForLineBudget(expandedLine, maxCharsPerLine);
|
|
|
|
for (const chunk of chunks) {
|
|
if (linesForDisplay.length >= lineBudget) {
|
|
const lastLineIndex = Math.max(0, lineBudget - 1);
|
|
const ellipsis = "...";
|
|
const existingLastLine = linesForDisplay[lastLineIndex] ?? "";
|
|
linesForDisplay[lastLineIndex] = `${existingLastLine.slice(
|
|
0,
|
|
Math.max(0, maxCharsPerLine - ellipsis.length)
|
|
)}${ellipsis}`;
|
|
return linesForDisplay.join("\n");
|
|
}
|
|
|
|
linesForDisplay.push(chunk);
|
|
}
|
|
}
|
|
|
|
return linesForDisplay.join("\n");
|
|
}
|
|
|
|
function createTypographyCandidate(
|
|
normalizedContent: string,
|
|
fontSize: number,
|
|
maxWidth: number,
|
|
charWidthRatio: number,
|
|
lineHeightRatio: number
|
|
): TypographyCandidate {
|
|
const lineHeight = Math.max(1, Math.round(fontSize * lineHeightRatio));
|
|
const maxCharsPerLine = Math.max(1, Math.floor(maxWidth / (fontSize * charWidthRatio)));
|
|
const renderedLineCount = countRenderedLines(normalizedContent, maxCharsPerLine);
|
|
|
|
return {
|
|
lineHeight,
|
|
maxCharsPerLine,
|
|
renderedLineCount,
|
|
};
|
|
}
|
|
|
|
function findFittingTypography(
|
|
normalizedContent: string,
|
|
startFontSize: number,
|
|
minFontSize: number,
|
|
maxWidth: number,
|
|
maxHeight: number,
|
|
fontStep: number,
|
|
charWidthRatio: number,
|
|
lineHeightRatio: number
|
|
) {
|
|
for (let fontSize = startFontSize; fontSize >= minFontSize; fontSize -= fontStep) {
|
|
const candidate = createTypographyCandidate(
|
|
normalizedContent,
|
|
fontSize,
|
|
maxWidth,
|
|
charWidthRatio,
|
|
lineHeightRatio
|
|
);
|
|
|
|
if (candidate.renderedLineCount * candidate.lineHeight <= maxHeight) {
|
|
return {
|
|
candidate,
|
|
fontSize,
|
|
};
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
export function fitCodeBlock({
|
|
language,
|
|
content,
|
|
maxWidth,
|
|
maxHeight,
|
|
maxFontSize = 16,
|
|
minFontSize = 8,
|
|
fontStep = DEFAULT_FONT_STEP,
|
|
charWidthRatio = DEFAULT_CODE_CHAR_WIDTH_RATIO,
|
|
lineHeightRatio = DEFAULT_CODE_LINE_HEIGHT_RATIO,
|
|
}: FitCodeBlockOptions): FittedCodeBlock {
|
|
const normalizedContent = normalizeCodeContent(language, content);
|
|
const highlightLanguage =
|
|
isValidJsonContent(normalizedContent) || seemsJsonLike(normalizedContent)
|
|
? "json"
|
|
: language;
|
|
const preferredMinFont = Math.max(1, minFontSize);
|
|
const hardMinFont = Math.max(1, Math.min(preferredMinFont, HARD_MIN_FONT_SIZE));
|
|
const startFont = Math.max(maxFontSize, preferredMinFont);
|
|
|
|
const preferredFit = findFittingTypography(
|
|
normalizedContent,
|
|
startFont,
|
|
preferredMinFont,
|
|
maxWidth,
|
|
maxHeight,
|
|
fontStep,
|
|
charWidthRatio,
|
|
lineHeightRatio
|
|
);
|
|
|
|
if (preferredFit) {
|
|
const highlighted = highlightCode(normalizedContent, highlightLanguage);
|
|
return {
|
|
text: normalizedContent,
|
|
highlightedHtml: highlighted.html,
|
|
prismLanguage: highlighted.prismLanguage,
|
|
fontSize: Math.round(preferredFit.fontSize * 10) / 10,
|
|
lineHeight: preferredFit.candidate.lineHeight,
|
|
fontFamily: DEFAULT_CODE_FONT_FAMILY,
|
|
};
|
|
}
|
|
|
|
if (hardMinFont < preferredMinFont) {
|
|
const emergencyFit = findFittingTypography(
|
|
normalizedContent,
|
|
preferredMinFont - fontStep,
|
|
hardMinFont,
|
|
maxWidth,
|
|
maxHeight,
|
|
fontStep,
|
|
charWidthRatio,
|
|
lineHeightRatio
|
|
);
|
|
|
|
if (emergencyFit) {
|
|
const highlighted = highlightCode(normalizedContent, highlightLanguage);
|
|
return {
|
|
text: normalizedContent,
|
|
highlightedHtml: highlighted.html,
|
|
prismLanguage: highlighted.prismLanguage,
|
|
fontSize: Math.round(emergencyFit.fontSize * 10) / 10,
|
|
lineHeight: emergencyFit.candidate.lineHeight,
|
|
fontFamily: DEFAULT_CODE_FONT_FAMILY,
|
|
};
|
|
}
|
|
}
|
|
|
|
const fallback = createTypographyCandidate(
|
|
normalizedContent,
|
|
hardMinFont,
|
|
maxWidth,
|
|
charWidthRatio,
|
|
lineHeightRatio
|
|
);
|
|
const fallbackLineBudget = Math.max(1, Math.floor(maxHeight / fallback.lineHeight));
|
|
const fallbackText = truncateContentToLineBudget(
|
|
normalizedContent,
|
|
fallbackLineBudget,
|
|
fallback.maxCharsPerLine
|
|
);
|
|
const highlighted = highlightCode(fallbackText, highlightLanguage);
|
|
return {
|
|
text: fallbackText,
|
|
highlightedHtml: highlighted.html,
|
|
prismLanguage: highlighted.prismLanguage,
|
|
fontSize: Math.round(hardMinFont * 10) / 10,
|
|
lineHeight: fallback.lineHeight,
|
|
fontFamily: DEFAULT_CODE_FONT_FAMILY,
|
|
};
|
|
}
|