shelving 1.216.2 → 1.217.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.
@@ -1,10 +1,11 @@
1
- import type { FileElementProps } from "../util/element.js";
1
+ import { type FileElementProps } from "../util/element.js";
2
2
  import { FileExtractor } from "./FileExtractor.js";
3
3
  /**
4
4
  * File extractor for Markdown files.
5
5
  * - Stores the raw markdown text as `content`; rendering happens at output time via `<Markup>`.
6
6
  * - Sets `title` from the first `# h1` heading if one is present — otherwise leaves it undefined
7
7
  * (a confident title only).
8
+ * - Sets `description` to the first prose paragraph as a plain-text summary (used for card listings and `<meta>`).
8
9
  */
9
10
  export declare class MarkdownExtractor extends FileExtractor {
10
11
  /** Markdown contributes the canonical title/path when merging same-key elements. */
@@ -18,3 +19,10 @@ export declare class MarkdownExtractor extends FileExtractor {
18
19
  * - Looks for a line starting with a single `#` followed by whitespace; doesn't match `##`+.
19
20
  */
20
21
  export declare function extractMarkdownTitle(text: string): string | undefined;
22
+ /**
23
+ * Find the first prose paragraph in a markdown source string and return it as a plain-text summary, or `undefined` if none.
24
+ * - Skips headings, fenced code blocks, and blank lines, then collects the first ordinary paragraph.
25
+ * - Renders that paragraph as markup and strips every tag, so inline syntax (`` `code` ``, `*emphasis*`, links) becomes plain text.
26
+ * - Collapses internal whitespace so the result is a single line suitable for a `description` / `<meta>` summary.
27
+ */
28
+ export declare function extractMarkdownDescription(text: string): string | undefined;
@@ -1,15 +1,19 @@
1
+ import { renderMarkup } from "../markup/render.js";
2
+ import { MARKUP_OPTIONS } from "../markup/rule/index.js";
3
+ import { getElementText } from "../util/element.js";
1
4
  import { FileExtractor } from "./FileExtractor.js";
2
5
  /**
3
6
  * File extractor for Markdown files.
4
7
  * - Stores the raw markdown text as `content`; rendering happens at output time via `<Markup>`.
5
8
  * - Sets `title` from the first `# h1` heading if one is present — otherwise leaves it undefined
6
9
  * (a confident title only).
10
+ * - Sets `description` to the first prose paragraph as a plain-text summary (used for card listings and `<meta>`).
7
11
  */
8
12
  export class MarkdownExtractor extends FileExtractor {
9
13
  /** Markdown contributes the canonical title/path when merging same-key elements. */
10
14
  priority = 10;
11
15
  extractProps(name, text) {
12
- return { name, title: extractMarkdownTitle(text), content: text };
16
+ return { name, title: extractMarkdownTitle(text), description: extractMarkdownDescription(text), content: text };
13
17
  }
14
18
  }
15
19
  /**
@@ -20,3 +24,36 @@ export function extractMarkdownTitle(text) {
20
24
  const match = text.match(/^#\s+(.+?)\s*$/m);
21
25
  return match?.[1];
22
26
  }
27
+ /**
28
+ * Find the first prose paragraph in a markdown source string and return it as a plain-text summary, or `undefined` if none.
29
+ * - Skips headings, fenced code blocks, and blank lines, then collects the first ordinary paragraph.
30
+ * - Renders that paragraph as markup and strips every tag, so inline syntax (`` `code` ``, `*emphasis*`, links) becomes plain text.
31
+ * - Collapses internal whitespace so the result is a single line suitable for a `description` / `<meta>` summary.
32
+ */
33
+ export function extractMarkdownDescription(text) {
34
+ const paragraph = [];
35
+ let fenced = false;
36
+ for (const line of text.split("\n")) {
37
+ const trimmed = line.trim();
38
+ // Toggle in/out of fenced code blocks — never treat their contents as the summary.
39
+ if (trimmed.startsWith("```") || trimmed.startsWith("~~~")) {
40
+ fenced = !fenced;
41
+ continue;
42
+ }
43
+ if (fenced)
44
+ continue;
45
+ // A blank line or heading ends the first paragraph (or is skipped while still searching for it).
46
+ if (!trimmed || trimmed.startsWith("#")) {
47
+ if (paragraph.length)
48
+ break;
49
+ continue;
50
+ }
51
+ paragraph.push(trimmed);
52
+ }
53
+ if (!paragraph.length)
54
+ return;
55
+ // Render the paragraph as markup then strip every tag, so inline syntax resolves to clean plain text.
56
+ const rendered = renderMarkup(paragraph.join(" "), MARKUP_OPTIONS);
57
+ const summary = getElementText(rendered).replace(/\s+/g, " ").trim();
58
+ return summary || undefined;
59
+ }
@@ -6,6 +6,7 @@ import { FileExtractor } from "./FileExtractor.js";
6
6
  * - Extracts exported, public, non-`_`-prefixed declarations as `tree-documentation` children.
7
7
  * - Overloaded declarations sharing a name are merged into a single `tree-documentation` with multiple `signatures`.
8
8
  * - Top-of-file JSDoc comment becomes the file's `content`.
9
+ * - Sets `description` (a plain-text summary from the first JSDoc paragraph) on the file and every `tree-documentation` child.
9
10
  * - Sets `title` on every `tree-documentation` child — `name()` for functions and methods, `name` for other kinds.
10
11
  * - The file element itself has no `title` — a TS source file has no confident title source; renderers fall back to `name`.
11
12
  */
@@ -1,12 +1,14 @@
1
1
  import ts from "typescript";
2
2
  import { requireSlug } from "../util/string.js";
3
3
  import { FileExtractor } from "./FileExtractor.js";
4
+ import { extractMarkdownDescription } from "./MarkdownExtractor.js";
4
5
  /**
5
6
  * File extractor that parses a TypeScript source file into a tree element.
6
7
  * - Uses the TypeScript compiler API to parse the AST.
7
8
  * - Extracts exported, public, non-`_`-prefixed declarations as `tree-documentation` children.
8
9
  * - Overloaded declarations sharing a name are merged into a single `tree-documentation` with multiple `signatures`.
9
10
  * - Top-of-file JSDoc comment becomes the file's `content`.
11
+ * - Sets `description` (a plain-text summary from the first JSDoc paragraph) on the file and every `tree-documentation` child.
10
12
  * - Sets `title` on every `tree-documentation` child — `name()` for functions and methods, `name` for other kinds.
11
13
  * - The file element itself has no `title` — a TS source file has no confident title source; renderers fall back to `name`.
12
14
  */
@@ -25,7 +27,7 @@ export class TypescriptExtractor extends FileExtractor {
25
27
  }
26
28
  // The file element itself gets no `title` — a TS source file has no confident title source (the filename isn't one),
27
29
  // so renderers fall back to `name`. The `tree-documentation` children each carry their own `title`.
28
- return { name, content, children: Array.from(byKey.values()) };
30
+ return { name, description: extractMarkdownDescription(content ?? ""), content, children: Array.from(byKey.values()) };
29
31
  }
30
32
  }
31
33
  /** Merge a newly-extracted overload into the existing documentation element with the same key. */
@@ -100,6 +102,7 @@ function _extractStatement(statement, source) {
100
102
  // Functions read as callable with `()`; other kinds use the bare name.
101
103
  title: kind === "function" ? `${name}()` : name,
102
104
  kind,
105
+ description: extractMarkdownDescription(jsDoc?.description ?? ""),
103
106
  content: _buildJSDocContent(jsDoc?.description, jsDoc?.unhandled),
104
107
  signatures: signature ? [signature] : undefined,
105
108
  params,
@@ -233,7 +236,14 @@ function _getClassMembers(statement, source) {
233
236
  members.push({
234
237
  type: "tree-documentation",
235
238
  key,
236
- props: { name, title: `${name}()`, content, kind: "method", signatures: [signature] },
239
+ props: {
240
+ name,
241
+ title: `${name}()`,
242
+ description: extractMarkdownDescription(memberJSDoc?.description ?? ""),
243
+ content,
244
+ kind: "method",
245
+ signatures: [signature],
246
+ },
237
247
  });
238
248
  }
239
249
  }
@@ -242,7 +252,14 @@ function _getClassMembers(statement, source) {
242
252
  members.push({
243
253
  type: "tree-documentation",
244
254
  key: requireSlug(name),
245
- props: { name, title: name, content, kind: "property", signatures: type ? [type] : undefined },
255
+ props: {
256
+ name,
257
+ title: name,
258
+ description: extractMarkdownDescription(memberJSDoc?.description ?? ""),
259
+ content,
260
+ kind: "property",
261
+ signatures: type ? [type] : undefined,
262
+ },
246
263
  });
247
264
  }
248
265
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shelving",
3
- "version": "1.216.2",
3
+ "version": "1.217.0",
4
4
  "author": "Dave Houlbrooke <dave@shax.com>",
5
5
  "repository": {
6
6
  "type": "git",
@@ -14,7 +14,7 @@
14
14
 
15
15
  /* Font faces */
16
16
  --font-sans: system-ui;
17
- --font-mono: "Courier New", "Courier", monospace;
17
+ --font-mono: ui-monospace, "SF Mono", "Consolas", "Menlo", monospace;
18
18
  --font-serif: "Palatino", "Garamond", serif;
19
19
 
20
20
  /* Semantic font faces */
@@ -4,6 +4,6 @@ import { type AbsolutePath } from "../../util/path.js";
4
4
  interface DirectoryCardProps extends DirectoryElementProps {
5
5
  path: AbsolutePath;
6
6
  }
7
- /** Card renderer for a `tree-directory` element. */
8
- export declare function DirectoryCard({ path, name, title, content }: DirectoryCardProps): ReactNode;
7
+ /** Card renderer for a `tree-directory` element — a summary card showing the heading and description. */
8
+ export declare function DirectoryCard({ path, name, title, description }: DirectoryCardProps): ReactNode;
9
9
  export {};
@@ -1,11 +1,10 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { joinPath } from "../../util/path.js";
3
3
  import { Card } from "../block/Card.js";
4
- import { Prose } from "../block/Prose.js";
4
+ import { Paragraph } from "../block/Paragraph.js";
5
5
  import { Subheading } from "../block/Subheading.js";
6
- import { Markup } from "../misc/Markup.js";
7
- /** Card renderer for a `tree-directory` element. */
8
- export function DirectoryCard({ path, name, title, content }) {
6
+ /** Card renderer for a `tree-directory` element — a summary card showing the heading and description. */
7
+ export function DirectoryCard({ path, name, title, description }) {
9
8
  const href = joinPath(path, name);
10
- return (_jsxs(Card, { href: href, children: [_jsx(Subheading, { children: title ?? name }), content && (_jsx(Prose, { children: _jsx(Markup, { children: content }) }))] }));
9
+ return (_jsxs(Card, { href: href, children: [_jsx(Subheading, { children: title ?? name }), description && _jsx(Paragraph, { children: description })] }));
11
10
  }
@@ -2,25 +2,20 @@ import type { ReactNode } from "react";
2
2
  import type { DirectoryElementProps } from "../../util/element.js";
3
3
  import { type AbsolutePath, joinPath } from "../../util/path.js";
4
4
  import { Card } from "../block/Card.js";
5
- import { Prose } from "../block/Prose.js";
5
+ import { Paragraph } from "../block/Paragraph.js";
6
6
  import { Subheading } from "../block/Subheading.js";
7
- import { Markup } from "../misc/Markup.js";
8
7
 
9
8
  interface DirectoryCardProps extends DirectoryElementProps {
10
9
  path: AbsolutePath;
11
10
  }
12
11
 
13
- /** Card renderer for a `tree-directory` element. */
14
- export function DirectoryCard({ path, name, title, content }: DirectoryCardProps): ReactNode {
12
+ /** Card renderer for a `tree-directory` element — a summary card showing the heading and description. */
13
+ export function DirectoryCard({ path, name, title, description }: DirectoryCardProps): ReactNode {
15
14
  const href = joinPath(path, name);
16
15
  return (
17
16
  <Card href={href}>
18
17
  <Subheading>{title ?? name}</Subheading>
19
- {content && (
20
- <Prose>
21
- <Markup>{content}</Markup>
22
- </Prose>
23
- )}
18
+ {description && <Paragraph>{description}</Paragraph>}
24
19
  </Card>
25
20
  );
26
21
  }
@@ -4,6 +4,6 @@ import { type AbsolutePath } from "../../util/path.js";
4
4
  interface DocumentationCardProps extends DocumentationElementProps {
5
5
  path: AbsolutePath;
6
6
  }
7
- /** Card renderer for a `tree-documentation` element. */
8
- export declare function DocumentationCard({ path, title, name, kind, content, signatures }: DocumentationCardProps): ReactNode;
7
+ /** Card renderer for a `tree-documentation` element — a summary card showing the heading, signatures, and description. */
8
+ export declare function DocumentationCard({ path, title, name, kind, description, signatures }: DocumentationCardProps): ReactNode;
9
9
  export {};
@@ -2,14 +2,13 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { joinPath } from "../../util/path.js";
3
3
  import { Card } from "../block/Card.js";
4
4
  import { Flex } from "../block/Flex.js";
5
+ import { Paragraph } from "../block/Paragraph.js";
5
6
  import { Preformatted } from "../block/Preformatted.js";
6
- import { Prose } from "../block/Prose.js";
7
7
  import { Subheading } from "../block/Subheading.js";
8
8
  import { Code } from "../inline/Code.js";
9
- import { Markup } from "../misc/Markup.js";
10
9
  import { DocumentationKind } from "./DocumentationKind.js";
11
- /** Card renderer for a `tree-documentation` element. */
12
- export function DocumentationCard({ path, title, name, kind, content, signatures }) {
10
+ /** Card renderer for a `tree-documentation` element — a summary card showing the heading, signatures, and description. */
11
+ export function DocumentationCard({ path, title, name, kind, description, signatures }) {
13
12
  const href = joinPath(path, name);
14
- return (_jsxs(Card, { href: href, children: [_jsx(Subheading, { children: _jsxs(Flex, { left: true, children: [_jsx(Code, { children: title ?? name }), kind && _jsx(DocumentationKind, { kind: kind })] }) }), signatures?.map(sig => (_jsx(Preformatted, { children: sig }, sig))), content && (_jsx(Prose, { children: _jsx(Markup, { children: content }) }))] }));
13
+ return (_jsxs(Card, { href: href, children: [_jsx(Subheading, { children: _jsxs(Flex, { left: true, children: [_jsx(Code, { children: title ?? name }), kind && _jsx(DocumentationKind, { kind: kind })] }) }), signatures?.map(sig => (_jsx(Preformatted, { children: sig }, sig))), description && _jsx(Paragraph, { children: description })] }));
15
14
  }
@@ -3,19 +3,18 @@ import type { DocumentationElementProps } from "../../util/element.js";
3
3
  import { type AbsolutePath, joinPath } from "../../util/path.js";
4
4
  import { Card } from "../block/Card.js";
5
5
  import { Flex } from "../block/Flex.js";
6
+ import { Paragraph } from "../block/Paragraph.js";
6
7
  import { Preformatted } from "../block/Preformatted.js";
7
- import { Prose } from "../block/Prose.js";
8
8
  import { Subheading } from "../block/Subheading.js";
9
9
  import { Code } from "../inline/Code.js";
10
- import { Markup } from "../misc/Markup.js";
11
10
  import { DocumentationKind } from "./DocumentationKind.js";
12
11
 
13
12
  interface DocumentationCardProps extends DocumentationElementProps {
14
13
  path: AbsolutePath;
15
14
  }
16
15
 
17
- /** Card renderer for a `tree-documentation` element. */
18
- export function DocumentationCard({ path, title, name, kind, content, signatures }: DocumentationCardProps): ReactNode {
16
+ /** Card renderer for a `tree-documentation` element — a summary card showing the heading, signatures, and description. */
17
+ export function DocumentationCard({ path, title, name, kind, description, signatures }: DocumentationCardProps): ReactNode {
19
18
  const href = joinPath(path, name);
20
19
  return (
21
20
  <Card href={href}>
@@ -28,11 +27,7 @@ export function DocumentationCard({ path, title, name, kind, content, signatures
28
27
  {signatures?.map(sig => (
29
28
  <Preformatted key={sig}>{sig}</Preformatted>
30
29
  ))}
31
- {content && (
32
- <Prose>
33
- <Markup>{content}</Markup>
34
- </Prose>
35
- )}
30
+ {description && <Paragraph>{description}</Paragraph>}
36
31
  </Card>
37
32
  );
38
33
  }
@@ -4,6 +4,6 @@ import { type AbsolutePath } from "../../util/path.js";
4
4
  interface FileCardProps extends FileElementProps {
5
5
  path: AbsolutePath;
6
6
  }
7
- /** Card renderer for a `tree-file` element. */
8
- export declare function FileCard({ path, name, title, content }: FileCardProps): ReactNode;
7
+ /** Card renderer for a `tree-file` element — a summary card showing the heading and description. */
8
+ export declare function FileCard({ path, name, title, description }: FileCardProps): ReactNode;
9
9
  export {};
@@ -1,11 +1,10 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { joinPath } from "../../util/path.js";
3
3
  import { Card } from "../block/Card.js";
4
- import { Prose } from "../block/Prose.js";
4
+ import { Paragraph } from "../block/Paragraph.js";
5
5
  import { Subheading } from "../block/Subheading.js";
6
- import { Markup } from "../misc/Markup.js";
7
- /** Card renderer for a `tree-file` element. */
8
- export function FileCard({ path, name, title, content }) {
6
+ /** Card renderer for a `tree-file` element — a summary card showing the heading and description. */
7
+ export function FileCard({ path, name, title, description }) {
9
8
  const href = joinPath(path, name);
10
- return (_jsxs(Card, { href: href, children: [_jsx(Subheading, { children: title ?? name }), content && (_jsx(Prose, { children: _jsx(Markup, { children: content }) }))] }));
9
+ return (_jsxs(Card, { href: href, children: [_jsx(Subheading, { children: title ?? name }), description && _jsx(Paragraph, { children: description })] }));
11
10
  }
@@ -2,25 +2,20 @@ import type { ReactNode } from "react";
2
2
  import type { FileElementProps } from "../../util/element.js";
3
3
  import { type AbsolutePath, joinPath } from "../../util/path.js";
4
4
  import { Card } from "../block/Card.js";
5
- import { Prose } from "../block/Prose.js";
5
+ import { Paragraph } from "../block/Paragraph.js";
6
6
  import { Subheading } from "../block/Subheading.js";
7
- import { Markup } from "../misc/Markup.js";
8
7
 
9
8
  interface FileCardProps extends FileElementProps {
10
9
  path: AbsolutePath;
11
10
  }
12
11
 
13
- /** Card renderer for a `tree-file` element. */
14
- export function FileCard({ path, name, title, content }: FileCardProps): ReactNode {
12
+ /** Card renderer for a `tree-file` element — a summary card showing the heading and description. */
13
+ export function FileCard({ path, name, title, description }: FileCardProps): ReactNode {
15
14
  const href = joinPath(path, name);
16
15
  return (
17
16
  <Card href={href}>
18
17
  <Subheading>{title ?? name}</Subheading>
19
- {content && (
20
- <Prose>
21
- <Markup>{content}</Markup>
22
- </Prose>
23
- )}
18
+ {description && <Paragraph>{description}</Paragraph>}
24
19
  </Card>
25
20
  );
26
21
  }
@@ -10,6 +10,6 @@
10
10
  background-color: var(--color-surface);
11
11
 
12
12
  /* Typography */
13
- font-weight: var(--weight-normal);
13
+ font-weight: inherit;
14
14
  font-size: var(--size-smaller);
15
15
  }
package/util/element.js CHANGED
@@ -22,7 +22,14 @@ export function getElementText(elements) {
22
22
  return elements;
23
23
  if (isElement(elements))
24
24
  return getElementText(elements.props.children);
25
- return Array.from(walkElements(elements)).map(getElementText).join("");
25
+ // Iterate the collection directly — `walkElements()` skips loose strings, so it would drop text that sits alongside elements.
26
+ if (isIterable(elements)) {
27
+ let text = "";
28
+ for (const child of elements)
29
+ text += getElementText(child);
30
+ return text;
31
+ }
32
+ return "";
26
33
  }
27
34
  export function* walkElements(elements) {
28
35
  if (isElement(elements))