diff --git a/apps/ios/EditorSource/src/document-nodes.test.mjs b/apps/ios/EditorSource/src/document-nodes.test.mjs index 3f04c419..10499c2f 100644 --- a/apps/ios/EditorSource/src/document-nodes.test.mjs +++ b/apps/ios/EditorSource/src/document-nodes.test.mjs @@ -11,6 +11,7 @@ import { MERGE_DIVIDER_NODE_TYPE, MergeDivider as SharedMergeDivider, } from "@edgeever/shared"; +import { createEdgeEverMathematics } from "@edgeever/shared/mathematics"; import { createIosImageGallery, ImageGallery, MergeDivider } from "./document-nodes.ts"; const galleryDoc = { @@ -30,6 +31,7 @@ const iosMarkdownManager = new MarkdownManager({ StarterKit.configure({ codeBlock: false, link: false }), Image, MergeDivider, + ...createEdgeEverMathematics(), createIosImageGallery(() => "en-US"), Markdown.configure({ markedOptions: { gfm: true }, @@ -81,4 +83,33 @@ describe("iOS document nodes share the web schema", () => { }); expect(persisted.content[0].content.map((node) => node.attrs.src)).toEqual(["/one.png", "/two.png"]); }); + + test("parses inline and block math the same as the web codec", () => { + const markdown = "Euler: $e^{i\\pi}+1=0$.\n\n$$\n\\frac{a}{b}\n$$"; + const webDoc = markdownToDoc(markdown); + const iosDoc = iosMarkdownManager.parse(markdown); + + expect(webDoc.content[0]?.content?.[1]).toMatchObject({ + type: "inlineMath", + attrs: { latex: "e^{i\\pi}+1=0" }, + }); + expect(webDoc.content[1]).toMatchObject({ + type: "blockMath", + attrs: { latex: "\\frac{a}{b}" }, + }); + expect(iosDoc.content.map((node) => node.type)).toEqual(webDoc.content.map((node) => node.type)); + expect(iosDoc.content[0]?.content?.[1]).toMatchObject(webDoc.content[0]?.content?.[1]); + expect(iosDoc.content[1]).toMatchObject(webDoc.content[1]); + expect(docToMarkdown(iosDoc)).toBe(docToMarkdown(webDoc)); + }); + + test("keeps currency dollar pairs as literal text like the web codec", () => { + const markdown = "Price: $100$"; + const webDoc = markdownToDoc(markdown); + const iosDoc = iosMarkdownManager.parse(markdown); + + expect(webDoc.content[0]?.content?.some((node) => node.type === "inlineMath")).toBe(false); + expect(iosDoc.content[0]?.content?.some((node) => node.type === "inlineMath")).toBe(false); + expect(docToMarkdown(iosDoc)).toBe(docToMarkdown(webDoc)); + }); }); diff --git a/apps/ios/EditorSource/src/main.ts b/apps/ios/EditorSource/src/main.ts index eabb0aec..85e1e43e 100644 --- a/apps/ios/EditorSource/src/main.ts +++ b/apps/ios/EditorSource/src/main.ts @@ -44,7 +44,7 @@ import { buildNoteImageCardMarkup, generateCardCss, } from "@edgeever/shared/note-image-card"; -import { createEdgeEverMathematics } from "./mathematics"; +import { createEdgeEverMathematics } from "@edgeever/shared/mathematics"; import { createIosImageGallery, MergeDivider } from "./document-nodes"; import { createImageInsertTransaction, groupUploadedImages, NATIVE_IMAGE_GALLERY_CSS } from "@edgeever/shared/native-image-gallery"; diff --git a/apps/ios/EditorSource/src/mathematics.ts b/apps/ios/EditorSource/src/mathematics.ts deleted file mode 100644 index 3dfae774..00000000 --- a/apps/ios/EditorSource/src/mathematics.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { BlockMath, InlineMath } from "@tiptap/extension-mathematics"; - -const isEscaped = (source: string, index: number) => { - let backslashCount = 0; - for (let cursor = index - 1; cursor >= 0 && source[cursor] === "\\"; cursor -= 1) { - backslashCount += 1; - } - return backslashCount % 2 === 1; -}; - -const findClosingDelimiter = (source: string, delimiter: "$" | "$$", from: number) => { - for (let index = from; index <= source.length - delimiter.length; index += 1) { - if (source.startsWith(delimiter, index) && !isEscaped(source, index)) return index; - } - return -1; -}; - -const EdgeEverInlineMath = InlineMath.extend({ - markdownTokenizer: { - name: "inlineMath", - level: "inline", - start: (source: string) => source.indexOf("$"), - tokenize: (source: string) => { - if (!source.startsWith("$") || source.startsWith("$$")) return undefined; - const closingIndex = findClosingDelimiter(source, "$", 1); - if (closingIndex < 0 || source[closingIndex + 1] === "$") return undefined; - const raw = source.slice(0, closingIndex + 1); - const latex = source.slice(1, closingIndex).trim(); - if (!latex || latex.includes("\n")) return undefined; - if (/^\d+(?:[.,]\d+)?$/u.test(latex)) return { type: "text", raw, text: raw }; - return { type: "inlineMath", raw, latex }; - }, - }, -}); - -const EdgeEverBlockMath = BlockMath.extend({ - markdownTokenizer: { - name: "blockMath", - level: "block", - start: (source: string) => source.indexOf("$$"), - tokenize: (source: string) => { - if (!source.startsWith("$$") || source.startsWith("$$$")) return undefined; - const closingIndex = findClosingDelimiter(source, "$$", 2); - if (closingIndex < 0) return undefined; - const latex = source.slice(2, closingIndex).trim(); - if (!latex) return undefined; - return { type: "blockMath", raw: source.slice(0, closingIndex + 2), latex }; - }, - }, -}); - -const katexOptions = { throwOnError: false, strict: "warn" as const, trust: false }; - -/** Keep in sync with packages/shared/src/mathematics.ts. */ -export const createEdgeEverMathematics = () => [ - EdgeEverBlockMath.configure({ katexOptions }), - EdgeEverInlineMath.configure({ katexOptions }), -];