@lotics/ui 11.8.2 → 11.8.3

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.
package/docs/catalog.md CHANGED
@@ -267,7 +267,7 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
267
267
  (the heading ramp: `xl` = `##` section title, `xxl` = `#` page/record title, `xxxl` = hero
268
268
  numbers), `weight`, `color`, alignment, tabular numerals.
269
269
  - **`markdown`** — `Markdown`: the single canonical markdown renderer for chat, apps, and
270
- `AgentRun`; rich on web via react-markdown/remark-gfm with copyable tables, plain-text on
270
+ `AgentRun`; rich GFM markdown on web with copyable tables, plain-text on
271
271
  native; takes a markdown `children` string.
272
272
  - **`markdown.css`** — import once for the web markdown styling.
273
273
  - **`format_date`** — `formatDate` / `parseDate` / `toISODate` + `DateFormatStyle`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lotics/ui",
3
- "version": "11.8.2",
3
+ "version": "11.8.3",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  "./tokens": "./src/tokens.ts",
@@ -238,9 +238,14 @@
238
238
  "dependencies": {
239
239
  "@lotics/docx": "^0.1.0",
240
240
  "@lotics/xlsx": "^0.1.0",
241
+ "mdast-util-from-markdown": "^2.0.3",
242
+ "mdast-util-gfm-footnote": "^2.1.0",
243
+ "mdast-util-gfm-strikethrough": "^2.0.0",
244
+ "mdast-util-gfm-table": "^2.0.0",
245
+ "mdast-util-gfm-task-list-item": "^2.0.0",
246
+ "micromark-extension-gfm": "^3.0.0",
241
247
  "pdfjs-dist": "^6.0.227",
242
- "react-markdown": "^10.0.0",
243
- "remark-gfm": "^4.0.0"
248
+ "react-markdown": "^10.0.0"
244
249
  },
245
250
  "peerDependencies": {
246
251
  "@react-native-picker/picker": ">=2.0.0",
@@ -279,6 +284,8 @@
279
284
  "lucide-react": "^0.562.0",
280
285
  "react-dom": "^19.2.0",
281
286
  "react-native-web": "^0.21.0",
287
+ "remark-parse": "^11.0.0",
288
+ "unified": "^11.0.5",
282
289
  "vite": "^7.2.4"
283
290
  }
284
291
  }
@@ -2,7 +2,7 @@ import "./markdown.css";
2
2
  import React, { useCallback, useRef, useState } from "react";
3
3
  import { View } from "react-native";
4
4
  import ReactMarkdown from "react-markdown";
5
- import remarkGfm from "remark-gfm";
5
+ import { remarkGfmSafe } from "./remark_gfm_safe";
6
6
  import { Icon } from "./icon";
7
7
  import { colors } from "./colors";
8
8
 
@@ -10,7 +10,7 @@ export function Markdown({ children }: { children: string }) {
10
10
  return (
11
11
  <View>
12
12
  <div className="ui-markdown">
13
- <ReactMarkdown remarkPlugins={[remarkGfm]} components={markdownComponents}>
13
+ <ReactMarkdown remarkPlugins={[remarkGfmSafe]} components={markdownComponents}>
14
14
  {children}
15
15
  </ReactMarkdown>
16
16
  </div>
@@ -0,0 +1,33 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { unified } from "unified";
3
+ import remarkParse from "remark-parse";
4
+ import { remarkGfmSafe } from "./remark_gfm_safe";
5
+
6
+ /** Parse markdown to an mdast tree and serialize it, so tests assert on the
7
+ * resulting document structure without depending on mdast node typings. */
8
+ function parse(markdown: string): string {
9
+ return JSON.stringify(unified().use(remarkParse).use(remarkGfmSafe).parse(markdown));
10
+ }
11
+
12
+ describe("remarkGfmSafe", () => {
13
+ it("autolinks a bare URL in normal text (micromark tokenizer, no lookbehind)", () => {
14
+ expect(parse("see https://example.com now")).toContain('"url":"https://example.com"');
15
+ });
16
+
17
+ it("autolinks a bare email in normal text (the case the removed sweep also handled)", () => {
18
+ expect(parse("contact me at a@b.com please")).toContain('"url":"mailto:a@b.com"');
19
+ });
20
+
21
+ it("autolinks a bare www link", () => {
22
+ expect(parse("visit www.example.com today")).toContain('"url":"http://www.example.com"');
23
+ });
24
+
25
+ it("still parses GFM tables", () => {
26
+ expect(parse("| a | b |\n| --- | --- |\n| 1 | 2 |")).toContain('"type":"table"');
27
+ });
28
+
29
+ it("still parses strikethrough and task lists", () => {
30
+ expect(parse("~~gone~~")).toContain('"type":"delete"');
31
+ expect(parse("- [x] done")).toContain('"checked":true');
32
+ });
33
+ });
@@ -0,0 +1,84 @@
1
+ import type { CompileContext, Extension, Token } from "mdast-util-from-markdown";
2
+ import { gfmFootnoteFromMarkdown } from "mdast-util-gfm-footnote";
3
+ import { gfmStrikethroughFromMarkdown } from "mdast-util-gfm-strikethrough";
4
+ import { gfmTableFromMarkdown } from "mdast-util-gfm-table";
5
+ import { gfmTaskListItemFromMarkdown } from "mdast-util-gfm-task-list-item";
6
+ import { gfm } from "micromark-extension-gfm";
7
+ import type { Plugin } from "unified";
8
+
9
+ /**
10
+ * `remark-gfm`, rebuilt so no regex lookbehind reaches the bundle.
11
+ *
12
+ * `remark-gfm` pulls `mdast-util-gfm-autolink-literal`, whose module contains an
13
+ * email autolink regex *literal* with a lookbehind `(?<=…)`. Lookbehind is a
14
+ * **parse-time** `SyntaxError` on WebKit older than Safari 16.4 — iOS Safari and
15
+ * every in-app WebView built on it (Zalo, Facebook). Because a regex literal is
16
+ * validated when the script is parsed, that one literal aborts parsing of the
17
+ * whole JS bundle, so the entire app fails to load, not just markdown. Merely
18
+ * disabling the code at runtime is not enough — the literal must never be
19
+ * bundled, i.e. the module must never be imported.
20
+ *
21
+ * So we compose GFM from its parts and provide the autolink-literal fromMarkdown
22
+ * handlers ourselves (below). These are exactly the upstream token→link
23
+ * handlers, minus the post-parse regex *sweep* that carried the lookbehind — the
24
+ * micromark tokenizer already autolinks URLs and emails in normal text without
25
+ * any lookbehind, so the common case is unchanged; only the rare residual cases
26
+ * that sweep additionally caught are lost. Tables, strikethrough, task lists,
27
+ * and footnotes are untouched. No toMarkdown extensions: this feeds
28
+ * `react-markdown`, which parses only and never serializes.
29
+ */
30
+
31
+ function enterLiteralAutolink(this: CompileContext, token: Token): void {
32
+ this.enter({ type: "link", title: null, url: "", children: [] }, token);
33
+ }
34
+
35
+ function enterLiteralAutolinkValue(this: CompileContext, token: Token): void {
36
+ this.config.enter.autolinkProtocol.call(this, token);
37
+ }
38
+
39
+ function exitLiteralAutolinkHttp(this: CompileContext, token: Token): void {
40
+ this.config.exit.autolinkProtocol.call(this, token);
41
+ }
42
+
43
+ function exitLiteralAutolinkWww(this: CompileContext, token: Token): void {
44
+ this.config.exit.data.call(this, token);
45
+ const node = this.stack[this.stack.length - 1];
46
+ if (node?.type === "link") node.url = "http://" + this.sliceSerialize(token);
47
+ }
48
+
49
+ function exitLiteralAutolinkEmail(this: CompileContext, token: Token): void {
50
+ this.config.exit.autolinkEmail.call(this, token);
51
+ }
52
+
53
+ function exitLiteralAutolink(this: CompileContext, token: Token): void {
54
+ this.exit(token);
55
+ }
56
+
57
+ const autolinkLiteralFromMarkdown: Extension = {
58
+ enter: {
59
+ literalAutolink: enterLiteralAutolink,
60
+ literalAutolinkEmail: enterLiteralAutolinkValue,
61
+ literalAutolinkHttp: enterLiteralAutolinkValue,
62
+ literalAutolinkWww: enterLiteralAutolinkValue,
63
+ },
64
+ exit: {
65
+ literalAutolink: exitLiteralAutolink,
66
+ literalAutolinkEmail: exitLiteralAutolinkEmail,
67
+ literalAutolinkHttp: exitLiteralAutolinkHttp,
68
+ literalAutolinkWww: exitLiteralAutolinkWww,
69
+ },
70
+ };
71
+
72
+ export const remarkGfmSafe: Plugin<[]> = function () {
73
+ const data = this.data();
74
+ const micromarkExtensions = (data.micromarkExtensions ??= []);
75
+ const fromMarkdownExtensions = (data.fromMarkdownExtensions ??= []);
76
+ micromarkExtensions.push(gfm());
77
+ fromMarkdownExtensions.push([
78
+ autolinkLiteralFromMarkdown,
79
+ gfmFootnoteFromMarkdown(),
80
+ gfmStrikethroughFromMarkdown(),
81
+ gfmTableFromMarkdown(),
82
+ gfmTaskListItemFromMarkdown(),
83
+ ]);
84
+ };