Skip to content
Closed
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
1 change: 1 addition & 0 deletions .github/VOUCHED.td
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,4 @@ github:jappyjan
github:justsomelegs
github:UtkarshUsername
github:SunkenInTime
github:bil0000
1 change: 0 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,6 @@ An empty database is a bad test. Seed your worktree's `.t3` with a copy of real
- Never make a PR unless the developer explicitly asks you to do so.
- Conventional commit titles, plain language: `fix(web): new threads no longer spike CPU`.
- Body: the problem in a sentence or two, then how you fixed it. End with the model and harness that did the work.
- **Rebase onto latest main before opening.** Stale branches conflict and burn a review round.
- UI changes need before/after images. Motion or timing needs a short video.
- One concern per PR. If the description says "also", split it.
- When babysitting: poll checks and comments newer than the last push, verify each bot finding against the source, fix real ones, dismiss false positives with a written reason. Stay quiet when nothing is new. Stop when the bots are green on the latest commit.
Expand Down
5 changes: 3 additions & 2 deletions apps/desktop/src/electron/ElectronMenu.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ describe("ElectronMenu", () => {
const electronMenu = yield* ElectronMenu.ElectronMenu;
const selectedItemId = yield* electronMenu.showContextMenu({
window: makeWindow(),
items: [{ id: "copy", label: "Copy" }],
items: [{ id: "copy", label: "Copy", accelerator: "Ctrl+Shift+C" }],
position: Option.none(),
});

Expand All @@ -98,7 +98,7 @@ describe("ElectronMenu", () => {
const electronMenu = yield* ElectronMenu.ElectronMenu;
const selectedItemId = yield* electronMenu.showContextMenu({
window: makeWindow(2),
items: [{ id: "copy", label: "Copy" }],
items: [{ id: "copy", label: "Copy", accelerator: "Ctrl+Shift+C" }],
position: Option.some({ x: 10.8, y: 20.2 }),
});

Expand All @@ -107,6 +107,7 @@ describe("ElectronMenu", () => {
assert.equal(popupOptions?.y, 40);
assert.deepEqual(buildFromTemplateMock.mock.calls[0]?.[0][0], {
label: "Copy",
accelerator: "Ctrl+Shift+C",
enabled: true,
click: buildFromTemplateMock.mock.calls[0]?.[0][0].click,
});
Expand Down
2 changes: 2 additions & 0 deletions apps/desktop/src/electron/ElectronMenu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ function normalizeContextMenuItems(source: readonly ContextMenuItem[]): ContextM
const normalizedItem: ContextMenuItem = {
id: sourceItem.id,
label: sourceItem.label,
...(sourceItem.accelerator ? { accelerator: sourceItem.accelerator } : {}),
destructive: sourceItem.destructive === true,
disabled: sourceItem.disabled === true,
};
Expand Down Expand Up @@ -150,6 +151,7 @@ export const make = Effect.gen(function* () {

const itemOption: Electron.MenuItemConstructorOptions = {
label: item.label,
...(item.accelerator ? { accelerator: item.accelerator } : {}),
enabled: !item.disabled,
};
if (item.children && item.children.length > 0) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,13 @@ import type { MarkdownNode } from "react-native-nitro-markdown/headless";

import { CopyTextButton } from "./CopyTextButton";
import { MarkdownTextPrimitive } from "./MarkdownTextPrimitive";
import {
nativeMarkdownDocumentRuns,
nativeMarkdownListItemBlocks,
nativeMarkdownTextRuns,
} from "./nativeMarkdownText";
import { nativeMarkdownDocumentRuns, nativeMarkdownListItemBlocks } from "./nativeMarkdownText";
import { NativeMarkdownSelectableText } from "./NativeMarkdownSelectableText.ios";
import type {
MarkdownCodeHighlighter,
MarkdownHighlightedToken,
NativeMarkdownTextStyle,
SelectableMarkdownSkill,
} from "./SelectableMarkdownText.types";

type HighlightedCode = ReadonlyArray<ReadonlyArray<MarkdownHighlightedToken>>;
Expand Down Expand Up @@ -48,12 +45,13 @@ function documentFor(node: MarkdownNode): MarkdownNode {

function SelectableNode(props: {
readonly node: MarkdownNode;
readonly skills: ReadonlyArray<SelectableMarkdownSkill>;
readonly textStyle: NativeMarkdownTextStyle;
readonly onLinkPress?: (href: string) => void;
}) {
return (
<NativeMarkdownSelectableText
runs={nativeMarkdownDocumentRuns(documentFor(props.node))}
runs={nativeMarkdownDocumentRuns(documentFor(props.node), props.skills)}
textStyle={props.textStyle}
onLinkPress={props.onLinkPress}
/>
Expand Down Expand Up @@ -322,6 +320,7 @@ function collectTableRows(node: MarkdownNode): MarkdownNode[] {

function NativeTable(props: {
readonly node: MarkdownNode;
readonly skills: ReadonlyArray<SelectableMarkdownSkill>;
readonly textStyle: NativeMarkdownTextStyle;
readonly onLinkPress?: (href: string) => void;
}) {
Expand Down Expand Up @@ -359,7 +358,7 @@ function NativeTable(props: {
}}
>
<NativeMarkdownSelectableText
runs={nativeMarkdownTextRuns(cell).map((run) =>
runs={nativeMarkdownDocumentRuns(documentFor(cell), props.skills).map((run) =>
rowIndex === 0 || cell.isHeader ? { ...run, bold: true } : run,
)}
textStyle={props.textStyle}
Expand All @@ -376,6 +375,7 @@ function NativeTable(props: {

function NativeMarkdownImage(props: {
readonly node: MarkdownNode;
readonly skills: ReadonlyArray<SelectableMarkdownSkill>;
readonly textStyle: NativeMarkdownTextStyle;
readonly onLinkPress?: (href: string) => void;
}) {
Expand All @@ -384,6 +384,7 @@ function NativeMarkdownImage(props: {
return (
<SelectableNode
node={props.node}
skills={props.skills}
textStyle={props.textStyle}
onLinkPress={props.onLinkPress}
/>
Expand Down Expand Up @@ -445,6 +446,7 @@ function inlineGroups(nodes: ReadonlyArray<MarkdownNode>): MarkdownNode[] {

function NativeMixedParagraph(props: {
readonly node: MarkdownNode;
readonly skills: ReadonlyArray<SelectableMarkdownSkill>;
readonly textStyle: NativeMarkdownTextStyle;
readonly onLinkPress?: (href: string) => void;
}) {
Expand All @@ -455,13 +457,15 @@ function NativeMixedParagraph(props: {
<NativeMarkdownImage
key={nodeKey(child, index)}
node={child}
skills={props.skills}
textStyle={props.textStyle}
onLinkPress={props.onLinkPress}
/>
) : (
<SelectableNode
key={nodeKey(child, index)}
node={child}
skills={props.skills}
textStyle={props.textStyle}
onLinkPress={props.onLinkPress}
/>
Expand All @@ -473,6 +477,7 @@ function NativeMixedParagraph(props: {

function NativeList(props: {
readonly node: MarkdownNode;
readonly skills: ReadonlyArray<SelectableMarkdownSkill>;
readonly textStyle: NativeMarkdownTextStyle;
readonly highlightCode: MarkdownCodeHighlighter;
readonly onLinkPress?: (href: string) => void;
Expand Down Expand Up @@ -534,6 +539,7 @@ function NativeList(props: {
<NativeMarkdownBlock
key={nodeKey(child, childIndex)}
node={child}
skills={props.skills}
textStyle={props.textStyle}
highlightCode={props.highlightCode}
onLinkPress={props.onLinkPress}
Expand All @@ -551,6 +557,7 @@ function NativeList(props: {

export function NativeMarkdownBlock(props: {
readonly node: MarkdownNode;
readonly skills: ReadonlyArray<SelectableMarkdownSkill>;
readonly textStyle: NativeMarkdownTextStyle;
readonly highlightCode: MarkdownCodeHighlighter;
readonly onLinkPress?: (href: string) => void;
Expand All @@ -566,6 +573,7 @@ export function NativeMarkdownBlock(props: {
<NativeMarkdownBlock
key={nodeKey(child, index)}
node={child}
skills={props.skills}
textStyle={props.textStyle}
highlightCode={props.highlightCode}
onLinkPress={props.onLinkPress}
Expand All @@ -587,6 +595,7 @@ export function NativeMarkdownBlock(props: {
return (
<NativeTable
node={props.node}
skills={props.skills}
textStyle={props.textStyle}
onLinkPress={props.onLinkPress}
/>
Expand All @@ -595,6 +604,7 @@ export function NativeMarkdownBlock(props: {
return (
<NativeMarkdownImage
node={props.node}
skills={props.skills}
textStyle={props.textStyle}
onLinkPress={props.onLinkPress}
/>
Expand Down Expand Up @@ -624,6 +634,7 @@ export function NativeMarkdownBlock(props: {
<NativeMarkdownBlock
key={nodeKey(child, index)}
node={child}
skills={props.skills}
textStyle={props.textStyle}
highlightCode={props.highlightCode}
onLinkPress={props.onLinkPress}
Expand All @@ -637,6 +648,7 @@ export function NativeMarkdownBlock(props: {
return (
<NativeList
node={props.node}
skills={props.skills}
textStyle={props.textStyle}
highlightCode={props.highlightCode}
onLinkPress={props.onLinkPress}
Expand All @@ -647,12 +659,14 @@ export function NativeMarkdownBlock(props: {
return (props.node.children ?? []).some((child) => child.type === "image") ? (
<NativeMixedParagraph
node={props.node}
skills={props.skills}
textStyle={props.textStyle}
onLinkPress={props.onLinkPress}
/>
) : (
<SelectableNode
node={props.node}
skills={props.skills}
textStyle={props.textStyle}
onLinkPress={props.onLinkPress}
/>
Expand All @@ -673,6 +687,7 @@ export function NativeMarkdownBlock(props: {
>
<SelectableNode
node={props.node}
skills={props.skills}
textStyle={props.textStyle}
onLinkPress={props.onLinkPress}
/>
Expand All @@ -690,6 +705,7 @@ export function NativeMarkdownBlock(props: {
<NativeMarkdownBlock
key={nodeKey(child, index)}
node={child}
skills={props.skills}
textStyle={props.textStyle}
highlightCode={props.highlightCode}
onLinkPress={props.onLinkPress}
Expand All @@ -703,6 +719,7 @@ export function NativeMarkdownBlock(props: {
return (
<SelectableNode
node={props.node}
skills={props.skills}
textStyle={props.textStyle}
onLinkPress={props.onLinkPress}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ export function SelectableMarkdownText({
chunk.kind === "rich" ? (
<NativeMarkdownBlock
node={chunk.node}
skills={skills}
textStyle={textStyle}
highlightCode={highlightCode}
onLinkPress={onLinkPress}
Expand Down
12 changes: 10 additions & 2 deletions apps/mobile/modules/t3-markdown-text/src/nativeMarkdownText.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,15 +70,22 @@ const EMPTY_CONTEXT: RunContext = {

const INLINE_HTML_TAG_PATTERN = /<\/?(?:kbd|mark|sub|sup|u)(?:\s[^>]*)?>/gi;

function decodeCodePoint(codePoint: number, entity: string): string {
if (!Number.isInteger(codePoint) || codePoint < 0 || codePoint > 0x10ffff) {
return entity;
}
return String.fromCodePoint(codePoint);
}

function decodeHtmlEntitiesOnce(value: string): string {
return value.replace(
/&(?:#(\d+)|#x([0-9a-f]+)|amp|apos|gt|lt|nbsp|quot);/gi,
(entity, decimal: string | undefined, hexadecimal: string | undefined) => {
if (decimal) {
return String.fromCodePoint(Number.parseInt(decimal, 10));
return decodeCodePoint(Number.parseInt(decimal, 10), entity);
}
if (hexadecimal) {
return String.fromCodePoint(Number.parseInt(hexadecimal, 16));
return decodeCodePoint(Number.parseInt(hexadecimal, 16), entity);
}
switch (entity.toLowerCase()) {
case "&amp;":
Expand Down Expand Up @@ -661,6 +668,7 @@ function appendDocumentBlock(
function containsRichBlock(node: MarkdownNode): boolean {
if (
node.type === "code_block" ||
node.type === "blockquote" ||
node.type === "table" ||
node.type === "image" ||
node.type === "horizontal_rule" ||
Expand Down
70 changes: 69 additions & 1 deletion apps/mobile/src/lib/nativeMarkdownText.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,22 @@ describe("nativeMarkdownTextRuns", () => {
]);
});

it.each([
["&#128512;", "😀"],
["&#x1f680;", "🚀"],
["&#9999999999;", "&#9999999999;"],
["&#x110000;", "&#x110000;"],
["&amp;#9999999999;", "&#9999999999;"],
["&amp;#x110000;", "&#x110000;"],
])("normalizes numeric entity %s without throwing", (content, expected) => {
const node: MarkdownNode = {
type: "paragraph",
children: [{ type: "text", content }],
};

expect(nativeMarkdownTextRuns(node)).toEqual([{ text: expected }]);
});

it("reads inline content from nested text nodes", () => {
const node: MarkdownNode = {
type: "paragraph",
Expand Down Expand Up @@ -173,6 +189,25 @@ describe("nativeMarkdownDocumentRuns", () => {
]);
});

it("decorates known skill references inside blockquotes", () => {
const node: MarkdownNode = {
type: "blockquote",
children: [
{
type: "paragraph",
children: [{ type: "text", content: "Use $ui for this." }],
},
],
};

expect(nativeMarkdownDocumentRuns(node, [{ name: "ui", displayName: "UI" }])).toContainEqual({
text: "$ui",
role: "body",
skillName: "ui",
skillLabel: "UI",
});
});

it("leaves unknown skill-like text unchanged", () => {
const node: MarkdownNode = {
type: "document",
Expand Down Expand Up @@ -328,7 +363,7 @@ describe("nativeMarkdownDocumentRuns", () => {
]);
});

it("includes quotes and fenced code in the same selectable string", () => {
it("preserves quotes and fenced code in document runs", () => {
const node: MarkdownNode = {
type: "document",
children: [
Expand Down Expand Up @@ -414,6 +449,39 @@ describe("nativeMarkdownListItemBlocks", () => {
});

describe("nativeMarkdownDocumentChunks", () => {
it("renders plain blockquotes as rich blocks so their marker spans wrapped lines", () => {
const blockquote: MarkdownNode = {
type: "blockquote",
beg: 0,
end: 120,
children: [
{
type: "paragraph",
children: [
{
type: "text",
content:
"Persistent random per-result keys are the strongest design, even when this text wraps.",
},
],
},
],
};

expect(
nativeMarkdownDocumentChunks({
type: "document",
children: [blockquote],
}),
).toEqual([
{
kind: "rich",
key: "rich:blockquote:0:120",
node: blockquote,
},
]);
});

it("keeps headings and plain lists in one selectable document", () => {
const document: MarkdownNode = {
type: "document",
Expand Down
Loading