@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 +1 -1
- package/package.json +10 -3
- package/src/markdown.web.tsx +2 -2
- package/src/remark_gfm_safe.test.ts +33 -0
- package/src/remark_gfm_safe.ts +84 -0
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
|
|
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.
|
|
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
|
}
|
package/src/markdown.web.tsx
CHANGED
|
@@ -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
|
|
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={[
|
|
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
|
+
};
|