@teriusu/rich-editor 0.9.1 → 0.9.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -7,9 +7,66 @@
7
7
  *
8
8
  * 노드는 atom이며 텍스트 콘텐츠(LaTeX 원문)를 자식 텍스트 노드로 보관한다.
9
9
  * NodeView가 KaTeX로 렌더링한다.
10
+ *
11
+ * ## 입력 경로 — 구세대(@seorii/tiptap)와 맞춘 것
12
+ *
13
+ * 이전할 때 **렌더만 옮기고 넣는 길이 통째로 빠져 있었다.** 문서는 열리는데 새 수식을
14
+ * 만들 방법이 없었다는 뜻이다. 구세대가 갖고 있던 네 경로를 여기로 되살렸다:
15
+ *
16
+ * | 경로 | 구세대 | 여기 |
17
+ * |---|---|---|
18
+ * | `$…$` | `makeInlineMathInputRule(/\$(.+)\$/)` | `addInputRules` (MathInline) |
19
+ * | `$$…$$` / `$$`+스페이스 | `makeBlockMathInputRule(/\$\$\s+$/)` | `addInputRules` (MathDisplay) |
20
+ * | 붙여넣기 `$…$` 자동 변환 | `plugin/mathPaste.js` | `mathPastePlugin` (아래) |
21
+ * | 선택 영역 감싸기/풀기 | `tiptap/setMath.js` | `toggleMathInline` 커맨드 |
22
+ *
23
+ * ## 편집은 모달로 — 구세대와 다른 유일한 지점
24
+ *
25
+ * 구세대는 노드 안에 **중첩 EditorView**를 띄워 LaTeX 원문을 그 자리에서 고치게 했다
26
+ * (`MathView` + `mathPlugin`/`mathSelectPlugin`, vanilla ProseMirror 400줄 남짓).
27
+ * 여기서는 그 대신 **호스트가 주는 프롬프트 훅**을 부른다 — 링크·이미지·카드 배경이
28
+ * 이미 쓰는 규약(`onPromptLink`, `promptBackground`)과 같은 모양이고, ProseMirror
29
+ * 내부(중첩 뷰·커서 브리지·backspace 체인)에 손대지 않아 유지비가 훨씬 싸다.
30
+ *
31
+ * 호스트가 `promptMath`를 안 주면 `window.prompt`로 떨어진다.
10
32
  */
11
- import { mergeAttributes, Node } from "@tiptap/core";
33
+ import { InputRule, Node, mergeAttributes } from "@tiptap/core";
34
+ import { Fragment, Slice } from "@tiptap/pm/model";
35
+ import { NodeSelection, Plugin, PluginKey } from "@tiptap/pm/state";
12
36
  import katex from "katex";
37
+ const INLINE_NAME = "math_inline";
38
+ const DISPLAY_NAME = "math_display";
39
+ /**
40
+ * 이스케이프한 `\$`는 수식으로 보지 않는다. 룩비하인드가 없는 런타임(구형 사파리)에서는
41
+ * 이스케이프 구분 없이 동작하는 쪽으로 떨어진다 — 구세대 `REGEX_INLINE_MATH_DOLLARS_ESCAPED`가
42
+ * 쓰던 방어를 그대로 옮겼다.
43
+ */
44
+ function safeRegExp(withLookbehind, fallback) {
45
+ try {
46
+ return new RegExp(withLookbehind);
47
+ }
48
+ catch {
49
+ return fallback;
50
+ }
51
+ }
52
+ /**
53
+ * `$x^2$` → 인라인 수식.
54
+ *
55
+ * ⚠️ 구세대 `REGEX_INLINE_MATH_DOLLARS`(`/\$(.+)\$/`)보다 **좁게** 잡는다. 그대로 쓰면
56
+ * `$5 와 $` 까지 친 순간 "5 와 " 가 수식이 돼 버린다(금액 표기가 흔한 게시판에서 바로 터진다).
57
+ * 그래서 내용의 **첫 글자와 끝 글자가 공백이 아닐 것**을 요구한다 — 붙여넣기 변환이 쓰는
58
+ * 판별과 같은 기준이다.
59
+ */
60
+ const INLINE_PATTERN = safeRegExp("(?<!\\\\)\\$([^$\\n\\s](?:[^$\\n]*[^$\\n\\s])?)(?<!\\\\)\\$$", /\$([^$\n\s](?:[^$\n]*[^$\n\s])?)\$$/);
61
+ /** `$$x^2$$` → 수식 블록(내용까지 한 번에). */
62
+ const DISPLAY_PATTERN = safeRegExp("(?<!\\\\)\\$\\$([^$\\n]+?)\\$\\$$", /\$\$([^$\n]+?)\$\$$/);
63
+ /**
64
+ * `$$` + 스페이스 → 빈 수식 블록 대신 **프롬프트**를 연다. 구세대 `REGEX_BLOCK_MATH_DOLLARS`.
65
+ *
66
+ * `^` 로 **줄 맨 앞**에 못 박는다(입력 규칙은 텍스트블록 시작부터의 문자열에 맞춘다).
67
+ * 안 그러면 문장 중간의 `$$ ` 가 문단을 쪼개며 블록을 끼워 넣는다.
68
+ */
69
+ const DISPLAY_OPEN_PATTERN = /^\$\$\s$/;
13
70
  function renderKatex(latex, displayMode) {
14
71
  try {
15
72
  return katex.renderToString(latex, {
@@ -23,28 +80,316 @@ function renderKatex(latex, displayMode) {
23
80
  return `<span class="math-error">${latex}</span>`;
24
81
  }
25
82
  }
83
+ /**
84
+ * 프롬프트 훅 꺼내기.
85
+ *
86
+ * 이 패키지는 `Storage` 타입 증강을 하지 않아 인덱스 접근이 막힌다(`editor.storage.card`도
87
+ * 같은 이유로 unknown 을 거친다). 두 노드가 같은 훅을 공유하므로 있는 쪽을 쓴다 — 호스트가
88
+ * 한쪽만 직접 등록했을 때도 편집이 죽지 않게 하려는 것이다.
89
+ */
90
+ function getPrompt(editor) {
91
+ const storage = editor.storage;
92
+ const inline = storage[INLINE_NAME];
93
+ const display = storage[DISPLAY_NAME];
94
+ return inline?.promptMath ?? display?.promptMath ?? null;
95
+ }
96
+ async function askMath(editor, current, displayMode) {
97
+ const prompt = getPrompt(editor);
98
+ if (prompt)
99
+ return prompt(current, displayMode);
100
+ return window.prompt(displayMode ? "수식 (LaTeX)" : "인라인 수식 (LaTeX)", current);
101
+ }
102
+ /** 프롬프트를 띄워 새 수식을 넣는다. 취소·빈 입력이면 아무것도 하지 않는다. */
103
+ async function promptInsert(editor, displayMode) {
104
+ const next = await askMath(editor, "", displayMode);
105
+ const latex = next?.trim();
106
+ if (!latex) {
107
+ editor.commands.focus();
108
+ return;
109
+ }
110
+ if (displayMode)
111
+ editor.chain().focus().insertMathDisplay(latex).run();
112
+ else
113
+ editor.chain().focus().insertMathInline(latex).run();
114
+ }
115
+ /** 이미 있는 노드의 LaTeX를 고친다. 빈 값으로 확인하면 노드를 지운다. */
116
+ async function promptEdit(editor, getPos, displayMode, currentLatex) {
117
+ const next = await askMath(editor, currentLatex, displayMode);
118
+ if (next == null) {
119
+ editor.commands.focus();
120
+ return;
121
+ }
122
+ const latex = next.trim();
123
+ const name = displayMode ? DISPLAY_NAME : INLINE_NAME;
124
+ editor
125
+ .chain()
126
+ .focus()
127
+ .command(({ tr, state }) => {
128
+ const pos = getPos();
129
+ if (pos == null)
130
+ return false;
131
+ // 프롬프트가 열려 있는 동안 문서가 바뀌었을 수 있다 — 크기를 캐시하지 않고 다시 읽는다.
132
+ const target = tr.doc.nodeAt(pos);
133
+ if (!target || target.type.name !== name)
134
+ return false;
135
+ const to = pos + target.nodeSize;
136
+ if (!latex) {
137
+ tr.delete(pos, to);
138
+ return true;
139
+ }
140
+ tr.replaceWith(pos, to, target.type.create(null, state.schema.text(latex)));
141
+ return true;
142
+ })
143
+ .run();
144
+ }
26
145
  function buildNodeView(displayMode) {
27
- return ({ node, HTMLAttributes }) => {
146
+ return ({ editor, node, getPos, HTMLAttributes, }) => {
28
147
  const tag = displayMode ? "math-display" : "math-inline";
29
148
  const dom = document.createElement(tag);
30
149
  Object.entries({ class: "math-node", ...HTMLAttributes }).forEach(([k, v]) => {
31
150
  if (v != null)
32
151
  dom.setAttribute(k, String(v));
33
152
  });
34
- dom.innerHTML = renderKatex(node.textContent, displayMode);
35
- return { dom };
153
+ // 노드는 atom이라 contentDOM이 없다. 안이 편집 가능한 채로 남으면 캐럿이 들어가
154
+ // KaTeX가 뱉은 마크업을 직접 건드리게 된다.
155
+ dom.contentEditable = "false";
156
+ let current = node;
157
+ let editing = false;
158
+ const paint = () => {
159
+ const latex = current.textContent.trim();
160
+ if (!latex) {
161
+ dom.classList.add("empty-math");
162
+ dom.innerHTML = `<span class="math-placeholder">${displayMode ? "수식 입력" : "수식"}</span>`;
163
+ return;
164
+ }
165
+ dom.classList.remove("empty-math");
166
+ dom.innerHTML = renderKatex(latex, displayMode);
167
+ };
168
+ // 클릭 한 번에 편집 — 구세대(`ensureFocus`)와 같은 즉시성.
169
+ // ⚠️ `preventDefault`/`stopPropagation`을 걸지 않는다. 그래야 ProseMirror가 NodeSelection도
170
+ // 같이 잡아, 프롬프트를 취소했을 때 노드가 선택된 채 남아 Backspace로 지울 수 있다.
171
+ dom.addEventListener("click", () => {
172
+ if (!editor.isEditable || editing)
173
+ return;
174
+ editing = true;
175
+ void promptEdit(editor, getPos, displayMode, current.textContent).finally(() => {
176
+ editing = false;
177
+ });
178
+ });
179
+ paint();
180
+ return {
181
+ dom,
182
+ update: (updated) => {
183
+ if (updated.type.name !== current.type.name)
184
+ return false;
185
+ current = updated;
186
+ paint();
187
+ return true;
188
+ },
189
+ // contentDOM이 없으므로 이 안의 DOM 변화는 전부 우리가 만든 것이다.
190
+ ignoreMutation: () => true,
191
+ };
36
192
  };
37
193
  }
194
+ /* -------------------------------------------------------------------------- */
195
+ /* 붙여넣기 변환 — 구세대 `plugin/mathPaste.js` 이식 */
196
+ /* -------------------------------------------------------------------------- */
197
+ const mathPasteKey = new PluginKey("mathPaste");
198
+ /**
199
+ * 워드·한글·LLM 답변에서 긁어온 `$…$` / `$$…$$` 텍스트를 수식 노드로 바꾼다.
200
+ *
201
+ * 구세대 구현을 그대로 옮겼다. 규칙이 잔손이 많아 보이는데 전부 오탐을 막으려던 것이다:
202
+ * 코드 블록·코드 마크 안은 건드리지 않고, 여는 `$` 바로 뒤나 닫는 `$` 바로 앞이 공백이면
203
+ * (`$5 과 $10` 같은 금액 표기) 수식으로 보지 않는다.
204
+ */
205
+ function mathPastePlugin() {
206
+ return new Plugin({
207
+ key: mathPasteKey,
208
+ props: {
209
+ transformPasted: (slice, view) => {
210
+ const { schema } = view.state;
211
+ const mathInline = schema.nodes[INLINE_NAME];
212
+ const mathDisplay = schema.nodes[DISPLAY_NAME];
213
+ if (!mathInline && !mathDisplay)
214
+ return slice;
215
+ let changed = false;
216
+ let blockChanged = false;
217
+ const transformFragment = (fragment, parent) => {
218
+ const nodes = [];
219
+ for (let index = 0; index < fragment.childCount; index++) {
220
+ const node = fragment.child(index);
221
+ if (parent?.type.spec.code ||
222
+ node.type.spec.code ||
223
+ node.type.name.startsWith("math_")) {
224
+ nodes.push(node);
225
+ continue;
226
+ }
227
+ // `$$` 만 있는 줄 … 본문 … `$$` 만 있는 줄 → 한 덩어리 수식 블록
228
+ if (mathDisplay && node.isTextblock && node.textContent.trim() === "$$") {
229
+ const source = [];
230
+ let endIndex = -1;
231
+ for (let offset = index + 1; offset < fragment.childCount; offset++) {
232
+ const candidate = fragment.child(offset);
233
+ if (candidate.type.spec.code ||
234
+ candidate.type.name.startsWith("math_") ||
235
+ !candidate.isTextblock) {
236
+ break;
237
+ }
238
+ const text = candidate.textContent;
239
+ if (text.trim() === "$$") {
240
+ endIndex = offset;
241
+ break;
242
+ }
243
+ source.push(text);
244
+ }
245
+ const text = source.join("\n").trim();
246
+ if (endIndex > -1 && text.length) {
247
+ nodes.push(mathDisplay.create(null, schema.text(text)));
248
+ index = endIndex;
249
+ changed = true;
250
+ blockChanged = true;
251
+ continue;
252
+ }
253
+ }
254
+ // 한 줄이 통째로 `$$…$$` 인 경우
255
+ if (mathDisplay && node.isTextblock) {
256
+ const match = /^\s*\$\$([\s\S]*?)\$\$\s*$/.exec(node.textContent);
257
+ const text = match?.[1]?.trim();
258
+ if (text) {
259
+ nodes.push(mathDisplay.create(null, schema.text(text)));
260
+ changed = true;
261
+ blockChanged = true;
262
+ continue;
263
+ }
264
+ }
265
+ if (node.isText) {
266
+ if (!mathInline || node.marks.some((mark) => mark.type.spec.code)) {
267
+ nodes.push(node);
268
+ continue;
269
+ }
270
+ const text = node.text || "";
271
+ const inlineNodes = [];
272
+ let cursor = 0;
273
+ let searchFrom = 0;
274
+ while (searchFrom < text.length) {
275
+ const open = text.indexOf("$", searchFrom);
276
+ if (open === -1)
277
+ break;
278
+ const beforeOpen = text[open - 1];
279
+ const afterOpen = text[open + 1];
280
+ if (beforeOpen === "\\" ||
281
+ beforeOpen === "$" ||
282
+ afterOpen === "$" ||
283
+ !afterOpen ||
284
+ /\s/.test(afterOpen)) {
285
+ searchFrom = open + 1;
286
+ continue;
287
+ }
288
+ let close = open + 1;
289
+ while (close < text.length) {
290
+ close = text.indexOf("$", close);
291
+ if (close === -1)
292
+ break;
293
+ const beforeClose = text[close - 1];
294
+ const afterClose = text[close + 1];
295
+ if (beforeClose === "\\" ||
296
+ beforeClose === "$" ||
297
+ afterClose === "$" ||
298
+ /\s/.test(beforeClose)) {
299
+ close++;
300
+ continue;
301
+ }
302
+ break;
303
+ }
304
+ if (close === -1)
305
+ break;
306
+ const source = text.slice(open + 1, close);
307
+ if (!source.trim()) {
308
+ searchFrom = close + 1;
309
+ continue;
310
+ }
311
+ if (open > cursor) {
312
+ inlineNodes.push(schema.text(text.slice(cursor, open), node.marks));
313
+ }
314
+ inlineNodes.push(mathInline.create(null, schema.text(source), node.marks));
315
+ cursor = close + 1;
316
+ searchFrom = cursor;
317
+ }
318
+ if (!inlineNodes.length) {
319
+ nodes.push(node);
320
+ continue;
321
+ }
322
+ if (cursor < text.length) {
323
+ inlineNodes.push(schema.text(text.slice(cursor), node.marks));
324
+ }
325
+ nodes.push(...inlineNodes);
326
+ changed = true;
327
+ continue;
328
+ }
329
+ if (!node.content.size) {
330
+ nodes.push(node);
331
+ continue;
332
+ }
333
+ const content = transformFragment(node.content, node);
334
+ nodes.push(content.eq(node.content) ? node : node.copy(content));
335
+ }
336
+ return Fragment.fromArray(nodes);
337
+ };
338
+ const content = transformFragment(slice.content, null);
339
+ if (!changed)
340
+ return slice;
341
+ // 블록이 바뀌었으면 열린 깊이를 유지할 수 없다 — 구세대와 같은 처리.
342
+ return blockChanged
343
+ ? new Slice(content, 0, 0)
344
+ : new Slice(content, slice.openStart, slice.openEnd);
345
+ },
346
+ },
347
+ });
348
+ }
349
+ function pushUniqueTarget(targets, from, to, text) {
350
+ if (targets.some((target) => target.from === from && target.to === to))
351
+ return;
352
+ targets.push({ from, to, text });
353
+ }
354
+ /** 현재 선택이 걸쳐 있는 인라인 수식 노드들. 셋 다 필요하다 — 노드선택/캐럿/범위선택. */
355
+ function collectMathTargets(state, mathInline) {
356
+ const targets = [];
357
+ const { selection } = state;
358
+ if (selection instanceof NodeSelection && selection.node.type === mathInline) {
359
+ pushUniqueTarget(targets, selection.from, selection.to, selection.node.textContent);
360
+ }
361
+ if (selection.$from.parent.type === mathInline) {
362
+ const depth = selection.$from.depth;
363
+ pushUniqueTarget(targets, selection.$from.before(depth), selection.$from.after(depth), selection.$from.parent.textContent);
364
+ }
365
+ state.doc.nodesBetween(selection.from, selection.to, (node, position) => {
366
+ if (node.type !== mathInline)
367
+ return;
368
+ pushUniqueTarget(targets, position, position + node.nodeSize, node.textContent);
369
+ return false;
370
+ });
371
+ return targets;
372
+ }
373
+ /* -------------------------------------------------------------------------- */
374
+ /* 노드 */
375
+ /* -------------------------------------------------------------------------- */
38
376
  export const MathInline = Node.create({
39
- name: "math_inline",
377
+ name: INLINE_NAME,
40
378
  group: "inline",
41
379
  inline: true,
42
380
  atom: true,
43
381
  content: "text*",
382
+ code: true,
44
383
  selectable: true,
45
384
  addOptions() {
46
385
  return {
47
386
  HTMLAttributes: { class: "math-node" },
387
+ promptMath: null,
388
+ };
389
+ },
390
+ addStorage() {
391
+ return {
392
+ promptMath: this.options.promptMath,
48
393
  };
49
394
  },
50
395
  parseHTML() {
@@ -54,19 +399,83 @@ export const MathInline = Node.create({
54
399
  return ["math-inline", mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0];
55
400
  },
56
401
  addNodeView() {
57
- return ({ node, HTMLAttributes }) => buildNodeView(false)({ node: node, HTMLAttributes });
402
+ return buildNodeView(false);
403
+ },
404
+ addInputRules() {
405
+ return [
406
+ new InputRule({
407
+ find: INLINE_PATTERN,
408
+ handler: ({ chain, range, match }) => {
409
+ const latex = (match[1] ?? "").trim();
410
+ if (!latex)
411
+ return;
412
+ chain().deleteRange(range).insertMathInline(latex).run();
413
+ },
414
+ }),
415
+ ];
416
+ },
417
+ addProseMirrorPlugins() {
418
+ // 두 노드가 다 등록돼도 붙여넣기 변환은 **한 번만** 걸어야 한다.
419
+ // 인라인 쪽에 두고, 안에서 두 노드 타입을 모두 처리한다.
420
+ return [mathPastePlugin()];
58
421
  },
59
422
  addCommands() {
60
423
  return {
61
424
  insertMathInline: (latex) => ({ commands }) => commands.insertContent({
62
- type: "math_inline",
425
+ type: INLINE_NAME,
63
426
  content: [{ type: "text", text: latex }],
64
427
  }),
428
+ promptMathInline: () => ({ editor }) => {
429
+ void promptInsert(editor, false);
430
+ return true;
431
+ },
432
+ toggleMathInline: () => ({ state, tr, dispatch }) => {
433
+ const mathInline = state.schema.nodes[INLINE_NAME];
434
+ if (!mathInline)
435
+ return false;
436
+ // 1) 이미 수식이면 원문 텍스트로 되돌린다.
437
+ const existing = collectMathTargets(state, mathInline);
438
+ if (existing.length) {
439
+ if (!dispatch)
440
+ return true;
441
+ for (const { from, to, text } of existing.sort((a, b) => b.from - a.from)) {
442
+ if (!text.length)
443
+ tr.delete(from, to);
444
+ else
445
+ tr.replaceWith(from, to, state.schema.text(text));
446
+ }
447
+ return true;
448
+ }
449
+ // 2) 아니면 선택한 텍스트를 감싼다.
450
+ if (state.selection.empty)
451
+ return false;
452
+ const targets = [];
453
+ state.doc.nodesBetween(state.selection.from, state.selection.to, (node, position) => {
454
+ if (!node.isTextblock)
455
+ return;
456
+ const from = Math.max(state.selection.from, position + 1);
457
+ const to = Math.min(state.selection.to, position + node.nodeSize - 1);
458
+ if (from >= to)
459
+ return;
460
+ const text = state.doc.textBetween(from, to, "");
461
+ if (!text.length)
462
+ return;
463
+ targets.push({ from, to, text });
464
+ });
465
+ if (!targets.length)
466
+ return false;
467
+ if (!dispatch)
468
+ return true;
469
+ for (const { from, to, text } of targets.sort((a, b) => b.from - a.from)) {
470
+ tr.replaceWith(from, to, mathInline.create(null, state.schema.text(text)));
471
+ }
472
+ return true;
473
+ },
65
474
  };
66
475
  },
67
476
  });
68
477
  export const MathDisplay = Node.create({
69
- name: "math_display",
478
+ name: DISPLAY_NAME,
70
479
  group: "block",
71
480
  atom: true,
72
481
  content: "text*",
@@ -76,6 +485,12 @@ export const MathDisplay = Node.create({
76
485
  addOptions() {
77
486
  return {
78
487
  HTMLAttributes: { class: "math-node" },
488
+ promptMath: null,
489
+ };
490
+ },
491
+ addStorage() {
492
+ return {
493
+ promptMath: this.options.promptMath,
79
494
  };
80
495
  },
81
496
  parseHTML() {
@@ -85,14 +500,44 @@ export const MathDisplay = Node.create({
85
500
  return ["math-display", mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0];
86
501
  },
87
502
  addNodeView() {
88
- return ({ node, HTMLAttributes }) => buildNodeView(true)({ node: node, HTMLAttributes });
503
+ return buildNodeView(true);
504
+ },
505
+ addInputRules() {
506
+ // InputRule 핸들러의 인자에는 `editor` 가 없다(`state`/`range`/`match`/커맨드뿐).
507
+ // 확장 컨텍스트에서 미리 잡아 둔다.
508
+ const { editor } = this;
509
+ return [
510
+ // 내용까지 한 번에 친 경우 — 프롬프트 없이 바로 만든다.
511
+ new InputRule({
512
+ find: DISPLAY_PATTERN,
513
+ handler: ({ chain, range, match }) => {
514
+ const latex = (match[1] ?? "").trim();
515
+ if (!latex)
516
+ return;
517
+ chain().deleteRange(range).insertMathDisplay(latex).run();
518
+ },
519
+ }),
520
+ // `$$` + 스페이스 — 빈 노드를 심는 대신 프롬프트를 연다.
521
+ // 취소하면 문서에 아무것도 남지 않는다(빈 수식 블록이 굴러다니지 않게).
522
+ new InputRule({
523
+ find: DISPLAY_OPEN_PATTERN,
524
+ handler: ({ commands, range }) => {
525
+ commands.deleteRange(range);
526
+ void promptInsert(editor, true);
527
+ },
528
+ }),
529
+ ];
89
530
  },
90
531
  addCommands() {
91
532
  return {
92
533
  insertMathDisplay: (latex) => ({ commands }) => commands.insertContent({
93
- type: "math_display",
534
+ type: DISPLAY_NAME,
94
535
  content: [{ type: "text", text: latex }],
95
536
  }),
537
+ promptMathDisplay: () => ({ editor }) => {
538
+ void promptInsert(editor, true);
539
+ return true;
540
+ },
96
541
  };
97
542
  },
98
543
  });
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=Math.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Math.test.d.ts","sourceRoot":"","sources":["../../src/extensions/Math.test.ts"],"names":[],"mappings":""}