Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/mobile/deps/README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
The Enriched Markdown tarball comes from Julius's [`fork/t3-mobile` branch](https://github.com/pingdotgg/enriched-markdown/tree/fork/t3-mobile). Its `t3-fork.json` records the exact source commit and upstream base. React block renderers and signed image sources (`renderers`, `resolveImageSource`) are upstream. The fork carries native streaming memory bounds (an 8 MiB measurement-key budget and coalesced pending renders), plus changes with an open upstream PR: the `onLinksChange` link list that link pills and iOS link menus read ([#1088](https://github.com/software-mansion/enriched-markdown/pull/1088)), Android inline image alignment ([#1089](https://github.com/software-mansion/enriched-markdown/pull/1089)), clipboard link metadata ([#962](https://github.com/software-mansion/enriched-markdown/pull/962)), and development-host autolinks ([#961](https://github.com/software-mansion/enriched-markdown/pull/961)).
The Enriched Markdown tarball comes from Julius's [`fork/t3-mobile` branch](https://github.com/pingdotgg/enriched-markdown/tree/fork/t3-mobile). Its `t3-fork.json` records the exact source commit and upstream base. React block renderers and signed image sources (`renderers`, `resolveImageSource`) are upstream. The fork carries native streaming memory bounds (an 8 MiB measurement-key budget and coalesced pending renders), plus changes with an open upstream PR: the `onLinksChange` link list that link pills and iOS link menus read ([#1088](https://github.com/software-mansion/enriched-markdown/pull/1088)), Android inline image alignment ([#1089](https://github.com/software-mansion/enriched-markdown/pull/1089)), clipboard link metadata ([#962](https://github.com/software-mansion/enriched-markdown/pull/962)), development-host autolinks ([#961](https://github.com/software-mansion/enriched-markdown/pull/961)), and `md4cFlags.preserveWindowsPaths`, which keeps backslashes in Windows drive paths in prose ([#1105](https://github.com/software-mansion/enriched-markdown/pull/1105), after [md4c#5](https://github.com/software-mansion-labs/md4c/pull/5)).

To replace it, commit the fork changes, run `yarn install --immutable` and `yarn workspace react-native-enriched-markdown prepare` in the fork checkout, then `node scripts/pack-t3.mjs /path/to/t3code/apps/mobile/deps`. Keep the package version aligned with Julius's `fork/t3-mobile` branch. Rename the archive to include the first eight characters of the source commit, such as `react-native-enriched-markdown-1.2.0-t3.16-8d2f84a8.tgz`, so each build has a distinct dependency path without version bumps. Update the tarball path in `apps/mobile/package.json` and `pnpm-workspace.yaml`'s `allowBuilds`, and run `vp i` to update the lockfile. Native clients need rebuilding when this dependency changes.
Binary file not shown.
Binary file not shown.
2 changes: 1 addition & 1 deletion apps/mobile/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@
"react-dom": "catalog:",
"react-native": "0.88.0",
"react-native-blob-util": "0.25.1",
"react-native-enriched-markdown": "file:deps/react-native-enriched-markdown-1.2.0-t3.16-558767d9.tgz",
"react-native-enriched-markdown": "file:deps/react-native-enriched-markdown-1.2.0-t3.16-29ddd424.tgz",
"react-native-gesture-handler": "~3.3.0",
"react-native-image-viewing": "^0.2.2",
"react-native-keyboard-controller": "1.22.8",
Expand Down
3 changes: 2 additions & 1 deletion apps/mobile/src/lib/markdownDocument.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ import type { Root, RootContent } from "mdast";
import { unified, type Processor } from "unified";
import remarkParse from "remark-parse";
import remarkGfm from "remark-gfm";
import { remarkKeepWindowsProsePaths } from "@t3tools/shared/markdownWindowsPaths";
import { isWindowsAbsolutePath } from "@t3tools/shared/path";

const parser = unified().use(remarkParse).use(remarkGfm);
const parser = unified().use(remarkParse).use(remarkGfm).use(remarkKeepWindowsProsePaths);
export type MarkdownNode = Root | RootContent;

interface MarkdownEdit {
Expand Down
8 changes: 8 additions & 0 deletions apps/mobile/src/lib/markdownHtml.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -330,3 +330,11 @@ describe("Windows paths in native Markdown", () => {
expect(markdownImageSources(markdown)).toEqual([image, paren, amp, htmlAmp]);
});
});

it("keeps backslashes in drive paths in mobile Markdown prose", () => {
const path = String.raw`Saved to C:\Users\me\.t3\_build\shot.png and C:/work\.config.`;

expect(parseMarkdownDocument(String.raw`${path} Then \*this\*.`).children).toMatchObject([
{ type: "paragraph", children: [{ type: "text", value: `${path} Then *this*.` }] },
]);
});
1 change: 1 addition & 0 deletions apps/mobile/src/native/EnrichedMarkdownText.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,7 @@ export function MobileEnrichedMarkdownText(props: SelectableMarkdownTextProps) {
latexMath: mathRenderingEnabled,
hardSoftBreaks: props.preserveSoftBreaks ?? false,
admonitions: true,
preserveWindowsPaths: true,
}}
enableTaskListItemToggle={false}
enableCodeBlockWrapToggle
Expand Down
4 changes: 4 additions & 0 deletions packages/shared/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -463,6 +463,10 @@
"types": "./src/proposedPlanText.ts",
"import": "./src/proposedPlanText.ts"
},
"./markdownWindowsPaths": {
"types": "./src/markdownWindowsPaths.ts",
"import": "./src/markdownWindowsPaths.ts"
},
"./markdownPipeline": {
"types": "./src/markdownPipeline.ts",
"import": "./src/markdownPipeline.ts"
Expand Down
49 changes: 1 addition & 48 deletions packages/shared/src/markdownPipeline.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import type { Paragraph, PhrasingContent, Root, RootContent } from "mdast";
import type { Processor } from "unified";
import { isWindowsAbsolutePath } from "./path.ts";
import { remarkKeepWindowsPaths } from "./markdownWindowsPaths.ts";
import type { PluggableList } from "unified";
import rehypeRaw from "rehype-raw";
import rehypeSanitize, { defaultSchema } from "rehype-sanitize";
Expand Down Expand Up @@ -302,52 +301,6 @@ function remarkNormalizeLinksAndTagInlineCode() {
};
}

interface DestinationCompileContext {
readonly stack: ReadonlyArray<{ readonly type: string; url?: string }>;
resume(): string;
sliceSerialize(token: unknown): string;
}

function keepWindowsPathDestination(this: DestinationCompileContext, token: unknown) {
const decoded = this.resume();
const authored = this.sliceSerialize(token);
const node = this.stack.at(-1);
// Character references still need decoding, so those destinations keep the parsed URL.
if (node)
node.url = isWindowsAbsolutePath(authored) && !authored.includes("&") ? authored : decoded;
}

const UNFINISHED_DRIVE_PATH = /(?:^|\W)[A-Za-z]:(?:[\\/]\S*)?$/;

interface EscapeCompileContext {
readonly stack: ReadonlyArray<{
readonly type: string;
children?: Array<{ type: string; value?: string }>;
}>;
}

function keepWindowsPathEscape(this: EscapeCompileContext) {
const text = this.stack.at(-1)?.children?.at(-1);
if (text?.type !== "text" || text.value === undefined) return;
const before = text.value.slice(0, -1);
if (UNFINISHED_DRIVE_PATH.test(before)) text.value = `${before}\\${text.value.slice(-1)}`;
}

/**
* CommonMark treats backslashes as escapes; preserve them as separators in Windows destinations
* and drive-path prose. Prose paths end at whitespace so later escapes still work.
*/
function remarkKeepWindowsPaths(this: Processor) {
const data = this.data();
(data.fromMarkdownExtensions ??= []).push({
exit: {
resourceDestinationString: keepWindowsPathDestination,
definitionDestinationString: keepWindowsPathDestination,
characterEscape: keepWindowsPathEscape,
},
});
}

export function shouldPreserveAssistantLineBreaks(text: string): boolean {
return /^★ Insight(?:\s|─)/mu.test(text);
}
67 changes: 67 additions & 0 deletions packages/shared/src/markdownWindowsPaths.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import type { Processor } from "unified";
import { isWindowsAbsolutePath } from "./path.ts";

interface DestinationCompileContext {
readonly stack: ReadonlyArray<{ readonly type: string; url?: string }>;
resume(): string;
sliceSerialize(token: unknown): string;
}

function keepWindowsPathDestination(this: DestinationCompileContext, token: unknown) {
const decoded = this.resume();
const authored = this.sliceSerialize(token);
const node = this.stack.at(-1);
// Character references still need decoding, so those destinations keep the parsed URL.
if (node)
node.url = isWindowsAbsolutePath(authored) && !authored.includes("&") ? authored : decoded;
}

// A drive path that has not reached whitespace yet: `C:`, `C:\Users\me`, `C:/work`.
const UNFINISHED_DRIVE_PATH = /(?:^|\W)[A-Za-z]:(?:[\\/]\S*)?$/;

interface EscapeCompileContext {
readonly stack: ReadonlyArray<{
readonly type: string;
children?: Array<{ type: string; value?: string }>;
}>;
}

function keepWindowsPathEscape(this: EscapeCompileContext) {
const text = this.stack.at(-1)?.children?.at(-1);
if (text?.type !== "text" || text.value === undefined) return;
const before = text.value.slice(0, -1);
if (UNFINISHED_DRIVE_PATH.test(before)) {
text.value = `${before}\\${text.value.slice(-1)}`;
}
}

function keepWindowsProsePathEscape(this: EscapeCompileContext) {
// Destinations, titles, and labels are buffered in a fragment.
if (this.stack.at(-1)?.type !== "fragment") keepWindowsPathEscape.call(this);
}

/**
* Keeps escapes inside drive paths in prose only, for parsers that already escape Windows
* destinations themselves. Prose paths end at whitespace: past that, nothing tells a path
* from a sentence, and `C:\x and \*this\*` must still read `*this*`.
*/
export function remarkKeepWindowsProsePaths(this: Processor) {
(this.data().fromMarkdownExtensions ??= []).push({
exit: { characterEscape: keepWindowsProsePathEscape },
});
}

/**
* CommonMark reads the `\.` in `C:\me\.t3\shot.png` as an escape. Every backslash in a
* Windows path is a separator, so link, image, and definition destinations that are
* Windows paths keep the text as written, and so do escapes inside drive paths.
*/
export function remarkKeepWindowsPaths(this: Processor) {
(this.data().fromMarkdownExtensions ??= []).push({
exit: {
resourceDestinationString: keepWindowsPathDestination,
definitionDestinationString: keepWindowsPathDestination,
characterEscape: keepWindowsPathEscape,
},
});
}
10 changes: 5 additions & 5 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion pnpm-workspace.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ allowBuilds:
node-pty: true
# Ships N-API prebuilds; its install script only rebuilds from source.
objc-js: false
"react-native-enriched-markdown@file:apps/mobile/deps/react-native-enriched-markdown-1.2.0-t3.16-558767d9.tgz": true
"react-native-enriched-markdown@file:apps/mobile/deps/react-native-enriched-markdown-1.2.0-t3.16-29ddd424.tgz": true
sharp: true
"@tsc-rs/typescript": true
utf-8-validate: false
Expand Down
Loading