feat: Support LaTeX and AsciiMath via MathJax on stem.excalidraw.com

This commit is contained in:
Daniel J. Geiger 2022-12-27 15:11:52 -06:00
parent c8370b394c
commit 86f5c2ebcf
84 changed files with 8331 additions and 289 deletions

View file

@ -13,7 +13,7 @@ import {
FontFamilyValues,
ExcalidrawTextContainer,
} from "../element/types";
import { getFontString, getUpdatedTimestamp, isTestEnv } from "../utils";
import { getUpdatedTimestamp, isTestEnv } from "../utils";
import { randomInteger, randomId } from "../random";
import { mutateElement, newElementWith } from "./mutateElement";
import { getNewGroupIdsForDuplication } from "../groups";
@ -26,12 +26,36 @@ import {
getBoundTextElementOffset,
getContainerDims,
getContainerElement,
measureText,
measureTextElement,
normalizeText,
wrapText,
wrapTextElement,
} from "./textElement";
import { BOUND_TEXT_PADDING, VERTICAL_ALIGN } from "../constants";
import { isArrowElement } from "./typeChecks";
import { getSubtypeMethods, isValidSubtype } from "../subtypes";
export const maybeGetSubtypeProps = (
obj: {
subtype?: ExcalidrawElement["subtype"];
customData?: ExcalidrawElement["customData"];
},
type: ExcalidrawElement["type"],
) => {
const data: typeof obj = {};
if ("subtype" in obj) {
data.subtype = obj.subtype;
}
if ("customData" in obj) {
data.customData = obj.customData;
}
if ("subtype" in data && !isValidSubtype(data.subtype, type)) {
delete data.subtype;
}
if (!("subtype" in data) && "customData" in data) {
delete data.customData;
}
return data as typeof obj;
};
type ElementConstructorOpts = MarkOptional<
Omit<ExcalidrawGenericElement, "id" | "type" | "isDeleted" | "updated">,
@ -44,6 +68,8 @@ type ElementConstructorOpts = MarkOptional<
| "version"
| "versionNonce"
| "link"
| "subtype"
| "customData"
>;
const _newElementBase = <T extends ExcalidrawElement>(
@ -69,8 +95,10 @@ const _newElementBase = <T extends ExcalidrawElement>(
...rest
}: ElementConstructorOpts & Omit<Partial<ExcalidrawGenericElement>, "type">,
) => {
const { subtype, customData } = rest;
// assign type to guard against excess properties
const element: Merge<ExcalidrawGenericElement, { type: T["type"] }> = {
...maybeGetSubtypeProps({ subtype, customData }, type),
id: rest.id || randomId(),
type,
x,
@ -103,8 +131,11 @@ export const newElement = (
opts: {
type: ExcalidrawGenericElement["type"];
} & ElementConstructorOpts,
): NonDeleted<ExcalidrawGenericElement> =>
_newElementBase<ExcalidrawGenericElement>(opts.type, opts);
): NonDeleted<ExcalidrawGenericElement> => {
const map = getSubtypeMethods(opts?.subtype);
map?.clean && map.clean(opts);
return _newElementBase<ExcalidrawGenericElement>(opts.type, opts);
};
/** computes element x/y offset based on textAlign/verticalAlign */
const getTextElementPositionOffsets = (
@ -138,8 +169,13 @@ export const newTextElement = (
containerId?: ExcalidrawTextContainer["id"];
} & ElementConstructorOpts,
): NonDeleted<ExcalidrawTextElement> => {
const map = getSubtypeMethods(opts?.subtype);
map?.clean && map.clean(opts);
const text = normalizeText(opts.text);
const metrics = measureText(text, getFontString(opts));
const metrics = measureTextElement(opts, {
text,
customData: opts.customData,
});
const offsets = getTextElementPositionOffsets(opts, metrics);
const textElement = newElementWith(
{
@ -181,7 +217,7 @@ const getAdjustedDimensions = (
width: nextWidth,
height: nextHeight,
baseline: nextBaseline,
} = measureText(nextText, getFontString(element), maxWidth);
} = measureTextElement(element, { text: nextText }, maxWidth);
const { textAlign, verticalAlign } = element;
let x: number;
let y: number;
@ -190,9 +226,9 @@ const getAdjustedDimensions = (
verticalAlign === VERTICAL_ALIGN.MIDDLE &&
!element.containerId
) {
const prevMetrics = measureText(
element.text,
getFontString(element),
const prevMetrics = measureTextElement(
element,
{ fontSize: element.fontSize },
maxWidth,
);
const offsets = getTextElementPositionOffsets(element, {
@ -268,11 +304,9 @@ export const refreshTextDimensions = (
) => {
const container = getContainerElement(textElement);
if (container) {
text = wrapText(
text = wrapTextElement(textElement, getMaxContainerWidth(container), {
text,
getFontString(textElement),
getMaxContainerWidth(container),
);
});
}
const dimensions = getAdjustedDimensions(textElement, text);
return { text, ...dimensions };
@ -336,6 +370,8 @@ export const newFreeDrawElement = (
simulatePressure: boolean;
} & ElementConstructorOpts,
): NonDeleted<ExcalidrawFreeDrawElement> => {
const map = getSubtypeMethods(opts?.subtype);
map?.clean && map.clean(opts);
return {
..._newElementBase<ExcalidrawFreeDrawElement>(opts.type, opts),
points: opts.points || [],
@ -353,6 +389,8 @@ export const newLinearElement = (
points?: ExcalidrawLinearElement["points"];
} & ElementConstructorOpts,
): NonDeleted<ExcalidrawLinearElement> => {
const map = getSubtypeMethods(opts?.subtype);
map?.clean && map.clean(opts);
return {
..._newElementBase<ExcalidrawLinearElement>(opts.type, opts),
points: opts.points || [],
@ -372,6 +410,8 @@ export const newImageElement = (
scale?: ExcalidrawImageElement["scale"];
} & ElementConstructorOpts,
): NonDeleted<ExcalidrawImageElement> => {
const map = getSubtypeMethods(opts?.subtype);
map?.clean && map.clean(opts);
return {
..._newElementBase<ExcalidrawImageElement>("image", opts),
// in the future we'll support changing stroke color for some SVG elements,