v3: comments say what the code can't — the documentation pass wanted JSDoc on every export and every field of an exported interface, which filled the code with restatements ("The dump.", "Whether it is loading.", "The props of X.", "Renders the X.") and module docs that told a reader nothing ("The hot and new feeds, and the props every main feed of the index takes."); some were wrong as well as vague. Every comment in api/, shared/ and src/ is rewritten under one rule, now stated in the README's "Comments" section: every module and export still has a JSDoc, and it gives context (who uses it and where), usage (what a caller must know: a precondition, what it leaves to the caller, a side effect) or its place in the chain (where its input comes from, where its output goes); a field, parameter, private function or local gets one only when it adds to its parent's, and context about a whole type goes on the type; a type's doc is phrased from the value's side ("Returned to Y by X when Z", "Passed to Y by X"); full sentences, as short as they can be, no label-colon-list; // goes wherever the code isn't obvious and says why, and the section banners and narration go. Every claim was checked against the code, which turned up some forty comments that were wrong: DumpCard's claimed it was only used by the hot and new feeds, the WebSocket welcome claimed to be a socket's first message (presence comes first), UpdateUserRequest said null removes a description (it keeps it), deletedDumpIds leaves out that it also holds dumps made private, THEME_COLOR claimed to mirror index.html's, and the like. The routers' JSDoc now says where they are mounted and what becomes of a thrown APIException. doc-lint.ts lets a field without a doc through; a module or export without one still fails. No code changes; 204 backend and 628 frontend tests pass, and doc:lint, deno check, deno lint and tsc are clean.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Markdown, rendered: GFM, @mentions as profile links, line breaks kept, and
|
||||
* links into the app kept in the app.
|
||||
* Renders the Markdown that `TextEditor` writes, wherever a dump's note, a
|
||||
* comment, a chat message or a description is shown.
|
||||
*
|
||||
* @module
|
||||
*/
|
||||
@@ -10,24 +10,25 @@ import { Link } from "react-router";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import remarkGfm from "remark-gfm";
|
||||
|
||||
/** The props of {@link Markdown}. */
|
||||
/** Passed to {@link Markdown} by any component that shows a user's text. */
|
||||
export interface MarkdownProps {
|
||||
/** The Markdown source. */
|
||||
children: string;
|
||||
/** An extra class. */
|
||||
className?: string;
|
||||
/** Whether it is a line-clamped teaser inside a card: images are dropped. */
|
||||
/**
|
||||
* Drops the images, for a line-clamped teaser in a card, where an image
|
||||
* would break the layout. The full text is one click away.
|
||||
*/
|
||||
inline?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* The mention rule the server notifies by (see shared/mention.ts), on parsed
|
||||
* text: an `@` not preceded by a word character, so an email address isn't a
|
||||
* mention. Link syntax is already parsed away here.
|
||||
* The rule of shared/mention.ts, which the server notifies by, applied to
|
||||
* parsed text: link syntax is already parsed away here, so only a preceding
|
||||
* word character (an email address) rules a mention out.
|
||||
*/
|
||||
const MENTION_RE = /(?<!\w)@(\w+)/g;
|
||||
|
||||
/** A node of the Markdown syntax tree, the part the plugins read. */
|
||||
/** The part of a Markdown syntax tree node that the plugins below read. */
|
||||
interface MdNode {
|
||||
type: string;
|
||||
value?: string;
|
||||
@@ -36,8 +37,8 @@ interface MdNode {
|
||||
}
|
||||
|
||||
/**
|
||||
* Turns bare @username into a link to that user. Works on the parsed tree
|
||||
* rather than the source text, so code keeps its @s verbatim and a URL that
|
||||
* Turns a bare `@username` into a link to that user's profile. It works on the
|
||||
* parsed tree rather than the source, so code keeps its `@`s and a URL that
|
||||
* contains one (`https://mastodon.social/@bob`) isn't split in two.
|
||||
*/
|
||||
function remarkMentions() {
|
||||
@@ -74,10 +75,9 @@ function remarkMentions() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Turns a single newline into a line break, as in chat everywhere: markdown's
|
||||
* own rule folds it into a space, so a message typed on three lines with
|
||||
* Shift+Enter came out as one. (Code keeps its newlines; it has no text
|
||||
* nodes.)
|
||||
* Turns a single newline into a line break, as chat apps do. Markdown folds it
|
||||
* into a space, so a message typed on three lines with Shift+Enter would show
|
||||
* as one. Code keeps its newlines, since it has no text nodes.
|
||||
*/
|
||||
function remarkBreaks() {
|
||||
const walk = (node: MdNode) => {
|
||||
@@ -97,17 +97,16 @@ function remarkBreaks() {
|
||||
return (tree: MdNode) => walk(tree);
|
||||
}
|
||||
|
||||
/** The remark plugins, in order. */
|
||||
const REMARK_PLUGINS = [remarkGfm, remarkMentions, remarkBreaks];
|
||||
|
||||
/**
|
||||
* Returns the path of a link into this app — relative, or absolute to this
|
||||
* origin — or null for anywhere else (the API's files included).
|
||||
* Returns the path of a link into this app, relative or absolute to this
|
||||
* origin, or null for anywhere else. The API's files and attachments count as
|
||||
* elsewhere, since they aren't routes of the app.
|
||||
*/
|
||||
function internalPath(href: string | undefined): string | null {
|
||||
if (!href) return null;
|
||||
if (href.startsWith("/") && !href.startsWith("//")) {
|
||||
// Files and attachments are the API's, not routes: a new tab, as before.
|
||||
return href.startsWith("/api/") ? null : href;
|
||||
}
|
||||
try {
|
||||
@@ -120,17 +119,15 @@ function internalPath(href: string | undefined): string | null {
|
||||
}
|
||||
}
|
||||
|
||||
/** react-markdown's element overrides. */
|
||||
type Components = React.ComponentProps<typeof ReactMarkdown>["components"];
|
||||
|
||||
/**
|
||||
* The element overrides, defined once at module scope to avoid recreation on
|
||||
* every render. Mentions and links to other pages here stay in the app; the
|
||||
* rest open in a new tab; a wide GFM table scrolls inside its own box.
|
||||
* Overrides react-markdown's elements. It is built once, since a new object on
|
||||
* each render would re-render every element.
|
||||
*/
|
||||
const MARKDOWN_COMPONENTS: Components = {
|
||||
// Mentions and links to other pages here stay in the app; the rest open
|
||||
// in a new tab.
|
||||
// Mentions and links to the app navigate in place, without a reload. The
|
||||
// rest open in a new tab.
|
||||
a: ({ href, children: linkChildren }) => {
|
||||
const path = internalPath(href);
|
||||
if (path) return <Link to={path}>{linkChildren}</Link>;
|
||||
@@ -151,20 +148,16 @@ const MARKDOWN_COMPONENTS: Components = {
|
||||
),
|
||||
};
|
||||
|
||||
/**
|
||||
* The element overrides of an inline rendering. Inline renderings are
|
||||
* line-clamped teasers inside cards, where an embedded image blows the layout
|
||||
* apart; images are dropped there — the full description is one click away on
|
||||
* the detail page.
|
||||
*/
|
||||
/** Overrides react-markdown's elements for an `inline` rendering. */
|
||||
const INLINE_MARKDOWN_COMPONENTS: Components = {
|
||||
...MARKDOWN_COMPONENTS,
|
||||
img: () => null,
|
||||
};
|
||||
|
||||
/**
|
||||
* Renders Markdown. Memoized: parsing runs the whole remark pipeline, and the
|
||||
* text rarely changes when the component around it re-renders.
|
||||
* Renders GFM with `@mentions` as profile links and single newlines kept. It
|
||||
* is memoized because parsing runs the whole remark pipeline, and the text
|
||||
* rarely changes when its parent re-renders.
|
||||
*/
|
||||
export const Markdown: React.MemoExoticComponent<
|
||||
(props: MarkdownProps) => React.JSX.Element
|
||||
|
||||
Reference in New Issue
Block a user