@reviewflow/ui 0.0.0-stage → 6.21.0
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/LICENSE +15 -0
- package/README.md +95 -2
- package/package.json +102 -4
- package/src/board/bucket-section.tsx +94 -0
- package/src/board/dataSource.ts +44 -0
- package/src/board/facts.ts +183 -0
- package/src/board/index.ts +12 -0
- package/src/board/pull-request-board.tsx +239 -0
- package/src/board/pull-request-row.tsx +181 -0
- package/src/components/branch-diff/diff-themes.ts +6 -0
- package/src/components/branch-diff/diff-worker-pool.tsx +12 -0
- package/src/components/branch-diff/diff-worker-pool.web.tsx +56 -0
- package/src/components/branch-diff/diff-worker.ts +7 -0
- package/src/components/branch-diff/use-collapsed-files.ts +49 -0
- package/src/components/branch-diff/use-diff-settings.tsx +97 -0
- package/src/components/branch-diff/whitespace-button.tsx +32 -0
- package/src/components/copyable-code-block.tsx +101 -0
- package/src/components/label-pill.tsx +84 -0
- package/src/components/link-in-row.ts +6 -0
- package/src/components/markdown-suggestion.tsx +60 -0
- package/src/components/markdown-text.tsx +1067 -0
- package/src/components/pull-request-actions-merge.tsx +341 -0
- package/src/components/pull-request-actions-review.tsx +218 -0
- package/src/components/pull-request-checks.tsx +86 -0
- package/src/components/pull-request-commit-files.ts +143 -0
- package/src/components/pull-request-commits.tsx +221 -0
- package/src/components/pull-request-conversation-card.tsx +220 -0
- package/src/components/pull-request-conversation-edit.tsx +147 -0
- package/src/components/pull-request-conversation-event.tsx +267 -0
- package/src/components/pull-request-conversation-person.tsx +28 -0
- package/src/components/pull-request-conversation-thread.tsx +158 -0
- package/src/components/pull-request-diff-annotations.tsx +443 -0
- package/src/components/pull-request-diff-collapse.ts +43 -0
- package/src/components/pull-request-diff-data.ts +174 -0
- package/src/components/pull-request-diff-impacts-summary.tsx +97 -0
- package/src/components/pull-request-diff-impacts.ts +45 -0
- package/src/components/pull-request-diff-jump.ts +87 -0
- package/src/components/pull-request-diff-lines.ts +103 -0
- package/src/components/pull-request-diff-overview.tsx +416 -0
- package/src/components/pull-request-diff-view.tsx +438 -0
- package/src/components/pull-request-diff-viewer.tsx +1374 -0
- package/src/components/pull-request-files.tsx +533 -0
- package/src/components/pull-request-markdown-input.tsx +91 -0
- package/src/components/pull-request-panel.tsx +1270 -0
- package/src/components/pull-request-part-notice.tsx +82 -0
- package/src/components/pull-request-reactions.tsx +178 -0
- package/src/components/pull-request-review.tsx +164 -0
- package/src/components/pull-request-revisions.tsx +273 -0
- package/src/components/pull-request-screen.tsx +299 -0
- package/src/components/pull-request-suggestion-actions.tsx +201 -0
- package/src/components/pull-request-timeline.tsx +270 -0
- package/src/components/repository-attachment-banner.tsx +42 -0
- package/src/components/scroll-parent.ts +50 -0
- package/src/css.d.ts +1 -0
- package/src/dataSource.ts +242 -0
- package/src/editor/alerts.ts +105 -0
- package/src/editor/code-editor.native.tsx +25 -0
- package/src/editor/code-editor.tsx +112 -0
- package/src/editor/codeEditorExtensions.ts +65 -0
- package/src/editor/codeHighlight.ts +71 -0
- package/src/editor/commands.ts +175 -0
- package/src/editor/decorations.ts +425 -0
- package/src/editor/editable-file.tsx +224 -0
- package/src/editor/emojiCompletion.ts +110 -0
- package/src/editor/extensions.ts +149 -0
- package/src/editor/frontmatter.ts +76 -0
- package/src/editor/inlineFormat.ts +114 -0
- package/src/editor/lineTypes.ts +55 -0
- package/src/editor/markdown-editor.css +511 -0
- package/src/editor/markdown-editor.native.tsx +29 -0
- package/src/editor/markdown-editor.tsx +295 -0
- package/src/editor/nodeClasses.ts +69 -0
- package/src/index.ts +75 -0
- package/src/markdown-github/flatten.ts +49 -0
- package/src/markdown-github/html-tokens.ts +93 -0
- package/src/markdown-github/pair-html.ts +354 -0
- package/src/markdown-github/safe-url.ts +15 -0
- package/src/markdown-github/table-columns.ts +23 -0
- package/src/markdown-github/toggle-task.ts +164 -0
- package/src/nativewind-env.d.ts +1 -0
- package/src/prStatus.ts +90 -0
- package/src/services/PullRequestDraftsProvider.tsx +179 -0
- package/src/services/PullRequestReviewDraftProvider.tsx +178 -0
- package/src/services/PullRequestSuggestionBatchProvider.tsx +112 -0
- package/src/services/emojify.ts +13 -0
- package/src/services/errorToMessage.ts +6 -0
- package/src/services/expiringDraftStore.ts +102 -0
- package/src/services/formatDataAge.ts +12 -0
- package/src/services/formatRelativeDate.ts +56 -0
- package/src/services/githubImages.tsx +33 -0
- package/src/services/isViewerAuthor.ts +7 -0
- package/src/slots.tsx +34 -0
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
import type { StateCommand } from "@codemirror/state";
|
|
2
|
+
import { EditorView } from "@codemirror/view";
|
|
3
|
+
import type { SVGIconElement } from "alouette";
|
|
4
|
+
import {
|
|
5
|
+
AccentScope,
|
|
6
|
+
IconButton,
|
|
7
|
+
ScopedTheme,
|
|
8
|
+
Separator,
|
|
9
|
+
Tooltip,
|
|
10
|
+
View,
|
|
11
|
+
useCurrentTheme,
|
|
12
|
+
} from "alouette";
|
|
13
|
+
import { CodeRegularIcon } from "alouette-icons/phosphor-icons/Code";
|
|
14
|
+
import { CodeBlockRegularIcon } from "alouette-icons/phosphor-icons/CodeBlock";
|
|
15
|
+
import { LinkSimpleRegularIcon } from "alouette-icons/phosphor-icons/LinkSimple";
|
|
16
|
+
import { ListBulletsRegularIcon } from "alouette-icons/phosphor-icons/ListBullets";
|
|
17
|
+
import { ListChecksRegularIcon } from "alouette-icons/phosphor-icons/ListChecks";
|
|
18
|
+
import { ListNumbersRegularIcon } from "alouette-icons/phosphor-icons/ListNumbers";
|
|
19
|
+
import { QuotesRegularIcon } from "alouette-icons/phosphor-icons/Quotes";
|
|
20
|
+
import { TextBRegularIcon } from "alouette-icons/phosphor-icons/TextB";
|
|
21
|
+
import { TextHOneRegularIcon } from "alouette-icons/phosphor-icons/TextHOne";
|
|
22
|
+
import { TextHThreeRegularIcon } from "alouette-icons/phosphor-icons/TextHThree";
|
|
23
|
+
import { TextHTwoRegularIcon } from "alouette-icons/phosphor-icons/TextHTwo";
|
|
24
|
+
import { TextItalicRegularIcon } from "alouette-icons/phosphor-icons/TextItalic";
|
|
25
|
+
import { TextStrikethroughRegularIcon } from "alouette-icons/phosphor-icons/TextStrikethrough";
|
|
26
|
+
import type { ReactNode } from "react";
|
|
27
|
+
import { useEffect, useEffectEvent, useRef } from "react";
|
|
28
|
+
import { setLineType, toggleInlineFormat } from "./commands.ts";
|
|
29
|
+
import {
|
|
30
|
+
changeBetween,
|
|
31
|
+
externalUpdate,
|
|
32
|
+
markdownEditorExtensions,
|
|
33
|
+
} from "./extensions.ts";
|
|
34
|
+
import "./markdown-editor.css";
|
|
35
|
+
|
|
36
|
+
interface FormatButtonProps {
|
|
37
|
+
icon: SVGIconElement;
|
|
38
|
+
label: string;
|
|
39
|
+
command: StateCommand;
|
|
40
|
+
viewRef: { current: EditorView | null };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** runs on the editor's own selection, then gives the focus back to it */
|
|
44
|
+
function FormatButton({
|
|
45
|
+
icon,
|
|
46
|
+
label,
|
|
47
|
+
command,
|
|
48
|
+
viewRef,
|
|
49
|
+
}: FormatButtonProps): ReactNode {
|
|
50
|
+
return (
|
|
51
|
+
<Tooltip
|
|
52
|
+
content={label}
|
|
53
|
+
render={(triggerProps) => (
|
|
54
|
+
<IconButton
|
|
55
|
+
{...triggerProps}
|
|
56
|
+
icon={icon}
|
|
57
|
+
aria-label={label}
|
|
58
|
+
variant="soft"
|
|
59
|
+
size="sm"
|
|
60
|
+
onPress={() => {
|
|
61
|
+
const view = viewRef.current;
|
|
62
|
+
if (view === null) return;
|
|
63
|
+
command(view);
|
|
64
|
+
view.focus();
|
|
65
|
+
}}
|
|
66
|
+
/>
|
|
67
|
+
)}
|
|
68
|
+
/>
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function FormatToolbar({
|
|
73
|
+
viewRef,
|
|
74
|
+
}: {
|
|
75
|
+
viewRef: { current: EditorView | null };
|
|
76
|
+
}): ReactNode {
|
|
77
|
+
return (
|
|
78
|
+
<View
|
|
79
|
+
role="toolbar"
|
|
80
|
+
aria-label="Formatting"
|
|
81
|
+
className="flex-row flex-wrap items-center gap-xxs"
|
|
82
|
+
>
|
|
83
|
+
<FormatButton
|
|
84
|
+
icon={<TextHOneRegularIcon />}
|
|
85
|
+
label="Heading 1"
|
|
86
|
+
command={setLineType("h1")}
|
|
87
|
+
viewRef={viewRef}
|
|
88
|
+
/>
|
|
89
|
+
<FormatButton
|
|
90
|
+
icon={<TextHTwoRegularIcon />}
|
|
91
|
+
label="Heading 2"
|
|
92
|
+
command={setLineType("h2")}
|
|
93
|
+
viewRef={viewRef}
|
|
94
|
+
/>
|
|
95
|
+
<FormatButton
|
|
96
|
+
icon={<TextHThreeRegularIcon />}
|
|
97
|
+
label="Heading 3"
|
|
98
|
+
command={setLineType("h3")}
|
|
99
|
+
viewRef={viewRef}
|
|
100
|
+
/>
|
|
101
|
+
<FormatButton
|
|
102
|
+
icon={<ListBulletsRegularIcon />}
|
|
103
|
+
label="Bullet list"
|
|
104
|
+
command={setLineType("ul")}
|
|
105
|
+
viewRef={viewRef}
|
|
106
|
+
/>
|
|
107
|
+
<FormatButton
|
|
108
|
+
icon={<ListNumbersRegularIcon />}
|
|
109
|
+
label="Numbered list"
|
|
110
|
+
command={setLineType("ol")}
|
|
111
|
+
viewRef={viewRef}
|
|
112
|
+
/>
|
|
113
|
+
<FormatButton
|
|
114
|
+
icon={<ListChecksRegularIcon />}
|
|
115
|
+
label="Task list"
|
|
116
|
+
command={setLineType("task")}
|
|
117
|
+
viewRef={viewRef}
|
|
118
|
+
/>
|
|
119
|
+
<FormatButton
|
|
120
|
+
icon={<QuotesRegularIcon />}
|
|
121
|
+
label="Quote"
|
|
122
|
+
command={setLineType("quote")}
|
|
123
|
+
viewRef={viewRef}
|
|
124
|
+
/>
|
|
125
|
+
<FormatButton
|
|
126
|
+
icon={<CodeBlockRegularIcon />}
|
|
127
|
+
label="Code block"
|
|
128
|
+
command={setLineType("code")}
|
|
129
|
+
viewRef={viewRef}
|
|
130
|
+
/>
|
|
131
|
+
<Separator vertical className="h-[20px]" />
|
|
132
|
+
<FormatButton
|
|
133
|
+
icon={<TextBRegularIcon />}
|
|
134
|
+
label="Bold (⌘B)"
|
|
135
|
+
command={toggleInlineFormat("bold")}
|
|
136
|
+
viewRef={viewRef}
|
|
137
|
+
/>
|
|
138
|
+
<FormatButton
|
|
139
|
+
icon={<TextItalicRegularIcon />}
|
|
140
|
+
label="Italic (⌘I)"
|
|
141
|
+
command={toggleInlineFormat("italic")}
|
|
142
|
+
viewRef={viewRef}
|
|
143
|
+
/>
|
|
144
|
+
<FormatButton
|
|
145
|
+
icon={<TextStrikethroughRegularIcon />}
|
|
146
|
+
label="Strikethrough (⌘⇧X)"
|
|
147
|
+
command={toggleInlineFormat("strikethrough")}
|
|
148
|
+
viewRef={viewRef}
|
|
149
|
+
/>
|
|
150
|
+
<FormatButton
|
|
151
|
+
icon={<CodeRegularIcon />}
|
|
152
|
+
label="Inline code (⌘E)"
|
|
153
|
+
command={toggleInlineFormat("code")}
|
|
154
|
+
viewRef={viewRef}
|
|
155
|
+
/>
|
|
156
|
+
<FormatButton
|
|
157
|
+
icon={<LinkSimpleRegularIcon />}
|
|
158
|
+
label="Link (⌘K)"
|
|
159
|
+
command={toggleInlineFormat("link")}
|
|
160
|
+
viewRef={viewRef}
|
|
161
|
+
/>
|
|
162
|
+
</View>
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export interface MarkdownEditorFieldProps {
|
|
167
|
+
value: string;
|
|
168
|
+
/** omitted for a read-only editor */
|
|
169
|
+
onChange?: (value: string) => void;
|
|
170
|
+
readOnly?: boolean;
|
|
171
|
+
"aria-label": string;
|
|
172
|
+
/** on the column holding the toolbar and the editor, which stretches to fill it */
|
|
173
|
+
className?: string;
|
|
174
|
+
/** a few lines high when empty, for a comment rather than a file */
|
|
175
|
+
compact?: boolean;
|
|
176
|
+
/** completes GitHub's `:shortcode:` emoji */
|
|
177
|
+
emoji?: boolean;
|
|
178
|
+
/** read at mount, like `readOnly` */
|
|
179
|
+
autoFocus?: boolean;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
interface LinkInkScopeProps {
|
|
183
|
+
children: ReactNode;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Links are inked in the info accent, like the rendered markdown. The info
|
|
188
|
+
* theme also retints the surface, borders and selection, so it is applied only
|
|
189
|
+
* long enough to capture its `--color-accent` into `--md-link`, then the
|
|
190
|
+
* current theme is restored around the editor.
|
|
191
|
+
*/
|
|
192
|
+
function LinkInkScope({ children }: LinkInkScopeProps): ReactNode {
|
|
193
|
+
const theme = useCurrentTheme();
|
|
194
|
+
return (
|
|
195
|
+
<AccentScope accent="info">
|
|
196
|
+
<div className="markdown-editor-link-ink">
|
|
197
|
+
<ScopedTheme theme={theme}>{children}</ScopedTheme>
|
|
198
|
+
</div>
|
|
199
|
+
</AccentScope>
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* A markdown file edited as the text it is: CodeMirror with the markdown styled
|
|
205
|
+
* in place and every marker left visible, ported from
|
|
206
|
+
* vscode-extension-markdown-beautiful-editor. Nothing
|
|
207
|
+
* is parsed into a tree and serialized back, so a save is byte for byte what
|
|
208
|
+
* was typed — the property a file under git, or a template the daemon parses,
|
|
209
|
+
* needs from an editor.
|
|
210
|
+
*
|
|
211
|
+
* **Web only.** CodeMirror owns a DOM subtree, which react-native-web cannot
|
|
212
|
+
* express, so it is mounted into a raw `div`; a native build would need a
|
|
213
|
+
* WebView around it. `markdown-editor.native.tsx` is what metro picks on a
|
|
214
|
+
* native platform: a plain `TextArea` with the same props, so the package still
|
|
215
|
+
* bundles there.
|
|
216
|
+
*
|
|
217
|
+
* `readOnly` is read at mount: a caller switching it remounts by key. A `value`
|
|
218
|
+
* that changes under a mounted editor is applied as one change rather than a
|
|
219
|
+
* remount, so the selection outside it survives, and is not reported back
|
|
220
|
+
* through `onChange`.
|
|
221
|
+
*/
|
|
222
|
+
export function MarkdownEditor({
|
|
223
|
+
value,
|
|
224
|
+
onChange,
|
|
225
|
+
readOnly = false,
|
|
226
|
+
"aria-label": ariaLabel,
|
|
227
|
+
className,
|
|
228
|
+
compact = false,
|
|
229
|
+
emoji = false,
|
|
230
|
+
autoFocus = false,
|
|
231
|
+
}: MarkdownEditorFieldProps): ReactNode {
|
|
232
|
+
const hostRef = useRef<HTMLDivElement>(null);
|
|
233
|
+
const viewRef = useRef<EditorView | null>(null);
|
|
234
|
+
const emitChange = useEffectEvent((next: string) => onChange?.(next));
|
|
235
|
+
// a remount for `readOnly` starts from the value of that render, not the first
|
|
236
|
+
const readValue = useEffectEvent(() => value);
|
|
237
|
+
const autoFocusRef = useRef(autoFocus);
|
|
238
|
+
|
|
239
|
+
useEffect(() => {
|
|
240
|
+
const host = hostRef.current;
|
|
241
|
+
if (host === null) return undefined;
|
|
242
|
+
|
|
243
|
+
const view = new EditorView({
|
|
244
|
+
parent: host,
|
|
245
|
+
// CodeMirror resolves its root through `assignedSlot`, so an editor slotted
|
|
246
|
+
// into a shadow root (a diff annotation) would read its selection from that
|
|
247
|
+
// shadow root, which does not see light DOM nodes: the cursor jumps and
|
|
248
|
+
// select-all is undone. The host's own tree is where the selection lives.
|
|
249
|
+
root: host.getRootNode() as Document | ShadowRoot,
|
|
250
|
+
doc: readValue(),
|
|
251
|
+
extensions: [
|
|
252
|
+
markdownEditorExtensions({
|
|
253
|
+
readOnly,
|
|
254
|
+
emoji,
|
|
255
|
+
onChange: (next) => {
|
|
256
|
+
emitChange(next);
|
|
257
|
+
},
|
|
258
|
+
}),
|
|
259
|
+
EditorView.contentAttributes.of({ "aria-label": ariaLabel }),
|
|
260
|
+
],
|
|
261
|
+
});
|
|
262
|
+
viewRef.current = view;
|
|
263
|
+
if (autoFocusRef.current) view.focus();
|
|
264
|
+
return () => {
|
|
265
|
+
view.destroy();
|
|
266
|
+
viewRef.current = null;
|
|
267
|
+
};
|
|
268
|
+
}, [readOnly, ariaLabel, emoji]);
|
|
269
|
+
|
|
270
|
+
useEffect(() => {
|
|
271
|
+
const view = viewRef.current;
|
|
272
|
+
if (view === null) return;
|
|
273
|
+
const changes = changeBetween(view.state.doc.toString(), value);
|
|
274
|
+
if (changes === null) return;
|
|
275
|
+
view.dispatch({ changes, annotations: externalUpdate.of(true) });
|
|
276
|
+
}, [value]);
|
|
277
|
+
|
|
278
|
+
return (
|
|
279
|
+
<View
|
|
280
|
+
className={className === undefined ? "gap-xs" : `gap-xs ${className}`}
|
|
281
|
+
>
|
|
282
|
+
{readOnly ? null : <FormatToolbar viewRef={viewRef} />}
|
|
283
|
+
<LinkInkScope>
|
|
284
|
+
<div
|
|
285
|
+
ref={hostRef}
|
|
286
|
+
className={(() => {
|
|
287
|
+
if (readOnly) return "markdown-editor markdown-editor-readonly";
|
|
288
|
+
if (compact) return "markdown-editor markdown-editor-compact";
|
|
289
|
+
return "markdown-editor";
|
|
290
|
+
})()}
|
|
291
|
+
/>
|
|
292
|
+
</LinkInkScope>
|
|
293
|
+
</View>
|
|
294
|
+
);
|
|
295
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lezer node name → `md-*` class, as data: a construct is styled by adding a
|
|
3
|
+
* row here, not a branch to the walk in `decorations.ts`.
|
|
4
|
+
*
|
|
5
|
+
* - `mark`: over the node's whole range, at the lowest precedence, so it is the
|
|
6
|
+
* outer element of every overlay (search match, selection).
|
|
7
|
+
* - `syntax`: over a delimiter leaf (`**`, `#`, `>`), at the highest
|
|
8
|
+
* precedence, so it renders *inside* the construct's mark and can be muted
|
|
9
|
+
* there.
|
|
10
|
+
* - `line`: on every line the node touches. Anything needing a block box — a
|
|
11
|
+
* background, a border, padding — is a line class, because a mark is split
|
|
12
|
+
* at line boundaries.
|
|
13
|
+
*
|
|
14
|
+
* The nodes whose class depends on context (`Task`, `TableCell`,
|
|
15
|
+
* `TableDelimiter`, `LinkLabel`, `LinkTitle`) are absent on purpose and handled
|
|
16
|
+
* in `decorations.ts`. Ported from vscode-extension-markdown-beautiful-editor
|
|
17
|
+
* (`src/shared/nodeClassMap.ts`), without math, footnotes and definition lists,
|
|
18
|
+
* whose grammars are not installed here.
|
|
19
|
+
*/
|
|
20
|
+
export type NodeDecorationLayer = "line" | "mark" | "syntax";
|
|
21
|
+
|
|
22
|
+
export interface NodeClassEntry {
|
|
23
|
+
layer: NodeDecorationLayer;
|
|
24
|
+
className: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const nodeClasses: Readonly<Record<string, NodeClassEntry>> = {
|
|
28
|
+
StrongEmphasis: { layer: "mark", className: "md-bold" },
|
|
29
|
+
Emphasis: { layer: "mark", className: "md-italic" },
|
|
30
|
+
InlineCode: { layer: "mark", className: "md-code" },
|
|
31
|
+
Escape: { layer: "mark", className: "md-escaped" },
|
|
32
|
+
Strikethrough: { layer: "mark", className: "md-strike" },
|
|
33
|
+
Autolink: { layer: "mark", className: "md-autolink" },
|
|
34
|
+
Link: { layer: "mark", className: "md-link" },
|
|
35
|
+
Image: { layer: "mark", className: "md-image" },
|
|
36
|
+
LinkReference: { layer: "mark", className: "md-link-def" },
|
|
37
|
+
ListItem: { layer: "mark", className: "md-list" },
|
|
38
|
+
Table: { layer: "mark", className: "md-table" },
|
|
39
|
+
CodeInfo: { layer: "mark", className: "md-code-info" },
|
|
40
|
+
|
|
41
|
+
ATXHeading1: { layer: "line", className: "md-heading md-h1" },
|
|
42
|
+
ATXHeading2: { layer: "line", className: "md-heading md-h2" },
|
|
43
|
+
ATXHeading3: { layer: "line", className: "md-heading md-h3" },
|
|
44
|
+
ATXHeading4: { layer: "line", className: "md-heading md-h4" },
|
|
45
|
+
ATXHeading5: { layer: "line", className: "md-heading md-h5" },
|
|
46
|
+
ATXHeading6: { layer: "line", className: "md-heading md-h6" },
|
|
47
|
+
// the title line only: `decorations.ts` gives the `===`/`---` underline its
|
|
48
|
+
// own class, so it keeps normal metrics rather than a heading-sized box
|
|
49
|
+
SetextHeading1: { layer: "line", className: "md-heading md-h1" },
|
|
50
|
+
SetextHeading2: { layer: "line", className: "md-heading md-h2" },
|
|
51
|
+
Blockquote: { layer: "line", className: "md-blockquote" },
|
|
52
|
+
HorizontalRule: { layer: "line", className: "md-hr" },
|
|
53
|
+
FencedCode: { layer: "line", className: "md-code-block" },
|
|
54
|
+
Frontmatter: { layer: "line", className: "md-frontmatter" },
|
|
55
|
+
|
|
56
|
+
EmphasisMark: { layer: "syntax", className: "md-syntax" },
|
|
57
|
+
CodeMark: { layer: "syntax", className: "md-syntax" },
|
|
58
|
+
HeaderMark: { layer: "syntax", className: "md-syntax" },
|
|
59
|
+
StrikethroughMark: { layer: "syntax", className: "md-syntax" },
|
|
60
|
+
TaskMarker: { layer: "syntax", className: "md-syntax" },
|
|
61
|
+
ListMark: { layer: "syntax", className: "md-syntax" },
|
|
62
|
+
LinkMark: { layer: "syntax", className: "md-syntax" },
|
|
63
|
+
QuoteMark: { layer: "syntax", className: "md-syntax" },
|
|
64
|
+
FrontmatterMark: { layer: "syntax", className: "md-syntax" },
|
|
65
|
+
URL: { layer: "syntax", className: "md-url" },
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export const nodeClassEntry = (nodeName: string): NodeClassEntry | undefined =>
|
|
69
|
+
nodeClasses[nodeName];
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
export type {
|
|
2
|
+
PullRequestDataSource,
|
|
3
|
+
PullRequestDiffRange,
|
|
4
|
+
PullRequestDiffRefusal,
|
|
5
|
+
PullRequestDiffState,
|
|
6
|
+
PullRequestDiffsState,
|
|
7
|
+
PullRequestImpactsState,
|
|
8
|
+
PullRequestReadingOrderState,
|
|
9
|
+
PullRequestReadState,
|
|
10
|
+
PullRequestShas,
|
|
11
|
+
RepositoryAttachment,
|
|
12
|
+
} from "./dataSource.ts";
|
|
13
|
+
export {
|
|
14
|
+
PullRequestDataSourceProvider,
|
|
15
|
+
usePullRequestDataSource,
|
|
16
|
+
} from "./dataSource.ts";
|
|
17
|
+
export type { MarkdownEditorProps, PullRequestSlots } from "./slots.tsx";
|
|
18
|
+
export { PullRequestSlotsProvider, usePullRequestSlots } from "./slots.tsx";
|
|
19
|
+
export type { PullRequestScreenProps } from "./components/pull-request-screen.tsx";
|
|
20
|
+
export { PullRequestScreen } from "./components/pull-request-screen.tsx";
|
|
21
|
+
export type { PullRequestTab } from "./components/pull-request-panel.tsx";
|
|
22
|
+
export {
|
|
23
|
+
PullRequestPanel,
|
|
24
|
+
isPullRequestTab,
|
|
25
|
+
} from "./components/pull-request-panel.tsx";
|
|
26
|
+
export { PullRequestTimeline } from "./components/pull-request-timeline.tsx";
|
|
27
|
+
export { PullRequestFiles } from "./components/pull-request-files.tsx";
|
|
28
|
+
export { PullRequestReview } from "./components/pull-request-review.tsx";
|
|
29
|
+
export { PullRequestCommits } from "./components/pull-request-commits.tsx";
|
|
30
|
+
export { DiffRefused } from "./components/pull-request-diff-view.tsx";
|
|
31
|
+
export { MarkdownText } from "./components/markdown-text.tsx";
|
|
32
|
+
export {
|
|
33
|
+
DiffSettingsProvider,
|
|
34
|
+
isDiffFileOrder,
|
|
35
|
+
} from "./components/branch-diff/use-diff-settings.tsx";
|
|
36
|
+
export type { DiffFileOrder } from "./components/branch-diff/use-diff-settings.tsx";
|
|
37
|
+
export type { DiffTheme } from "./components/branch-diff/diff-themes.ts";
|
|
38
|
+
export {
|
|
39
|
+
diffThemeNames,
|
|
40
|
+
diffThemeOptions,
|
|
41
|
+
diffThemes,
|
|
42
|
+
} from "./components/branch-diff/diff-themes.ts";
|
|
43
|
+
// DiffWorkerPool is the `@reviewflow/ui/diff-worker-pool` subpath, not exported here: its web
|
|
44
|
+
// variant pulls @pierre/diffs and shiki, which this index would put in every importer's main
|
|
45
|
+
// bundle. A lazily loaded diff viewer imports the subpath.
|
|
46
|
+
export type { CollapsedFiles } from "./components/branch-diff/use-collapsed-files.ts";
|
|
47
|
+
export { useCollapsedFiles } from "./components/branch-diff/use-collapsed-files.ts";
|
|
48
|
+
export { WhitespaceButton } from "./components/branch-diff/whitespace-button.tsx";
|
|
49
|
+
export { CopyableCodeBlock } from "./components/copyable-code-block.tsx";
|
|
50
|
+
export { PullRequestReviewDraftProvider } from "./services/PullRequestReviewDraftProvider.tsx";
|
|
51
|
+
export type { DraftStore } from "./services/PullRequestDraftsProvider.tsx";
|
|
52
|
+
export { PullRequestDraftsProvider } from "./services/PullRequestDraftsProvider.tsx";
|
|
53
|
+
export type {
|
|
54
|
+
DraftStorage,
|
|
55
|
+
ExpiringDraftStoreOptions,
|
|
56
|
+
} from "./services/expiringDraftStore.ts";
|
|
57
|
+
export { createExpiringDraftStore } from "./services/expiringDraftStore.ts";
|
|
58
|
+
export { PullRequestSuggestionBatchProvider } from "./services/PullRequestSuggestionBatchProvider.tsx";
|
|
59
|
+
export { emojify } from "./services/emojify.ts";
|
|
60
|
+
export { errorToMessage } from "./services/errorToMessage.ts";
|
|
61
|
+
export { isViewerAuthor } from "./services/isViewerAuthor.ts";
|
|
62
|
+
export type { PrStatus } from "./prStatus.ts";
|
|
63
|
+
export { prStatus, prStatusAccents, prStatusLabels } from "./prStatus.ts";
|
|
64
|
+
export * from "./board/index.ts";
|
|
65
|
+
export { MarkdownEditor } from "./editor/markdown-editor.tsx";
|
|
66
|
+
export type { MarkdownEditorFieldProps } from "./editor/markdown-editor.tsx";
|
|
67
|
+
export { CodeEditor } from "./editor/code-editor.tsx";
|
|
68
|
+
export type { CodeEditorProps } from "./editor/code-editor.tsx";
|
|
69
|
+
export type { CodeLanguage } from "./editor/codeEditorExtensions.ts";
|
|
70
|
+
export { EditableFile } from "./editor/editable-file.tsx";
|
|
71
|
+
export type {
|
|
72
|
+
EditableFileProps,
|
|
73
|
+
EditableFileSaveResult,
|
|
74
|
+
} from "./editor/editable-file.tsx";
|
|
75
|
+
export { RepositoryAttachmentBanner } from "./components/repository-attachment-banner.tsx";
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { GhBlockNode } from "./pair-html.ts";
|
|
2
|
+
|
|
3
|
+
/** The text of a run, as a link label or a `<pre>`'s source. */
|
|
4
|
+
export function textOf(nodes: GhBlockNode[]): string {
|
|
5
|
+
return nodes
|
|
6
|
+
.map((node): string => {
|
|
7
|
+
if (node.type === "text" || node.type === "inlineCode") return node.value;
|
|
8
|
+
if (node.type === "image") return node.alt;
|
|
9
|
+
if (node.type === "break") return "\n";
|
|
10
|
+
if (node.type === "element") {
|
|
11
|
+
if (node.tag === "br") return "\n";
|
|
12
|
+
if (node.tag === "img") return node.attrs.alt ?? "";
|
|
13
|
+
}
|
|
14
|
+
return "children" in node ? textOf(node.children) : "";
|
|
15
|
+
})
|
|
16
|
+
.join("");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface GhImage {
|
|
20
|
+
src: string;
|
|
21
|
+
alt: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Every image in a run, through links and `<picture>`. */
|
|
25
|
+
export const collectImages = (nodes: GhBlockNode[]): GhImage[] =>
|
|
26
|
+
nodes.flatMap((node): GhImage[] => {
|
|
27
|
+
if (node.type === "image") return [{ src: node.src, alt: node.alt }];
|
|
28
|
+
if (node.type === "element" && node.tag === "img") {
|
|
29
|
+
const src = node.attrs.src;
|
|
30
|
+
return src ? [{ src, alt: node.attrs.alt ?? "" }] : [];
|
|
31
|
+
}
|
|
32
|
+
if ("children" in node) return collectImages(node.children);
|
|
33
|
+
return [];
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const isImageOnly = (node: GhBlockNode): boolean => {
|
|
37
|
+
if (node.type === "image" || node.type === "break") return true;
|
|
38
|
+
if (node.type === "text") return node.value.trim() === "";
|
|
39
|
+
if (node.type === "link") return node.children.every(isImageOnly);
|
|
40
|
+
if (node.type === "element") {
|
|
41
|
+
if (["br", "img", "picture"].includes(node.tag)) return true;
|
|
42
|
+
return node.tag === "a" && node.children.every(isImageOnly);
|
|
43
|
+
}
|
|
44
|
+
return false;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/** A run of badges and pictures: laid out as a row rather than as text. */
|
|
48
|
+
export const isImageRun = (nodes: GhBlockNode[]): boolean =>
|
|
49
|
+
collectImages(nodes).length > 0 && nodes.every(isImageOnly);
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { Parser } from "htmlparser2";
|
|
2
|
+
|
|
3
|
+
export type HtmlToken =
|
|
4
|
+
| { kind: "close"; tag: string }
|
|
5
|
+
| { kind: "open"; tag: string; attrs: Record<string, string> }
|
|
6
|
+
| { kind: "text"; value: string };
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The tags and text of one raw `html` / `inlineHtml` value, in order. Comments
|
|
10
|
+
* are dropped.
|
|
11
|
+
*
|
|
12
|
+
* The parser's implied closes at the end of its input are discarded: a value
|
|
13
|
+
* holding only `<details>` is not a complete element, its `</details>` is in a
|
|
14
|
+
* later sibling, so closing it here would cut the pairing short. Void elements
|
|
15
|
+
* and the parser's own mid-stream rules (`<li>` after `<li>`) do close.
|
|
16
|
+
*
|
|
17
|
+
* The parser ignores a close tag it has no open tag for, and a value holding
|
|
18
|
+
* only `</details>` has none: `openTags`, the caller's currently open tags
|
|
19
|
+
* outermost first, are replayed into the parser first so it can match them.
|
|
20
|
+
*/
|
|
21
|
+
export const tokenizeHtml = (
|
|
22
|
+
value: string,
|
|
23
|
+
openTags: string[] = [],
|
|
24
|
+
): HtmlToken[] => {
|
|
25
|
+
const tokens: HtmlToken[] = [];
|
|
26
|
+
let ended = false;
|
|
27
|
+
let replaying = true;
|
|
28
|
+
|
|
29
|
+
const parser = new Parser({
|
|
30
|
+
onopentag(tag, attrs) {
|
|
31
|
+
if (!ended && !replaying) tokens.push({ kind: "open", tag, attrs });
|
|
32
|
+
},
|
|
33
|
+
onclosetag(tag) {
|
|
34
|
+
if (!ended && !replaying) tokens.push({ kind: "close", tag });
|
|
35
|
+
},
|
|
36
|
+
ontext(text) {
|
|
37
|
+
if (replaying) return;
|
|
38
|
+
const last = tokens.at(-1);
|
|
39
|
+
if (last?.kind === "text") last.value += text;
|
|
40
|
+
else tokens.push({ kind: "text", value: text });
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
openTags.forEach((tag) => {
|
|
45
|
+
parser.write(`<${tag}>`);
|
|
46
|
+
});
|
|
47
|
+
replaying = false;
|
|
48
|
+
parser.write(value);
|
|
49
|
+
ended = true;
|
|
50
|
+
parser.end();
|
|
51
|
+
|
|
52
|
+
return tokens;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* The markdown parser leaves `&` and `😀` as typed, which is not
|
|
57
|
+
* what GitHub shows. The text goes through the html parser with its `<`
|
|
58
|
+
* escaped, so only entities are decoded and no tag is ever read out of it.
|
|
59
|
+
*/
|
|
60
|
+
export const decodeEntities = (value: string): string =>
|
|
61
|
+
value.includes("&")
|
|
62
|
+
? tokenizeHtml(value.replaceAll("<", "<"))
|
|
63
|
+
.map((token) => (token.kind === "text" ? token.value : ""))
|
|
64
|
+
.join("")
|
|
65
|
+
: value;
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* What follows the complete HTML comments a value starts with, or null when it
|
|
69
|
+
* does not start with one or nothing follows. CommonMark makes a line starting
|
|
70
|
+
* with `<!--` an html block that runs to the end of the line, so the markdown
|
|
71
|
+
* after a comment (`- [ ] <!-- id -->[a link](url)`) reaches here unparsed,
|
|
72
|
+
* where GitHub reads it as the rest of the paragraph.
|
|
73
|
+
*/
|
|
74
|
+
export const textAfterLeadingComments = (value: string): string | null => {
|
|
75
|
+
let end = 0;
|
|
76
|
+
let leading = true;
|
|
77
|
+
|
|
78
|
+
const parser = new Parser({
|
|
79
|
+
oncomment() {
|
|
80
|
+
if (!leading) return;
|
|
81
|
+
if (value.slice(end, parser.startIndex).trim() === "") {
|
|
82
|
+
end = parser.endIndex + 1;
|
|
83
|
+
} else {
|
|
84
|
+
leading = false;
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
parser.write(value);
|
|
89
|
+
parser.end();
|
|
90
|
+
|
|
91
|
+
const rest = value.slice(end);
|
|
92
|
+
return end > 0 && rest.trim() !== "" ? rest : null;
|
|
93
|
+
};
|