@effected/markdown 0.2.5 → 0.3.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.
@@ -6,6 +6,80 @@ import { parsePassResult } from "./Markdown.js";
6
6
  import { Effect, Result, Schema } from "effect";
7
7
 
8
8
  //#region src/MarkdownDocument.ts
9
+ /**
10
+ * A heading-delimited span from {@link MarkdownDocument.sections}: the
11
+ * heading, its depth, the source `range` the section occupies and the
12
+ * root-level blocks that follow the heading inside it.
13
+ *
14
+ * @remarks
15
+ * A section runs from its heading's start offset to the start of the next
16
+ * root-level heading of equal or shallower depth, or to the end of the
17
+ * source. Deeper headings nest inside, so a parent section's `range` and
18
+ * `children` include its subsections — the list is flat, in document order,
19
+ * with `depth` carrying the hierarchy.
20
+ *
21
+ * Instances come from {@link MarkdownDocument.sections} and the two finders;
22
+ * there is no reason to construct one directly.
23
+ *
24
+ * @public
25
+ */
26
+ var DocumentSection = class {
27
+ /** The heading that opens the section. */
28
+ heading;
29
+ /** The heading's depth, and the section's. */
30
+ depth;
31
+ /** The whole section including its heading — spliceable by `MarkdownEdit`. */
32
+ range;
33
+ /** The root-level blocks after the heading, subsections included. */
34
+ children;
35
+ /**
36
+ * The heading's plain-text content.
37
+ *
38
+ * @remarks
39
+ * The same derivation as {@link DocumentHeading.text}: text and inline-code
40
+ * values concatenated, image `alt` where present, a hard break as one space,
41
+ * nothing for raw HTML or footnote references. So `## **1.2.3**` reads
42
+ * `1.2.3`.
43
+ */
44
+ text;
45
+ /**
46
+ * The section WITHOUT its heading: from the heading's end offset to the
47
+ * section's end.
48
+ *
49
+ * @remarks
50
+ * This is the span a "give me the release notes under this version" reader
51
+ * wants, and the one that is fiddly to derive by hand.
52
+ */
53
+ bodyRange;
54
+ /** The document source these ranges index into. */
55
+ #documentSource;
56
+ constructor(options) {
57
+ this.heading = options.heading;
58
+ this.depth = options.depth;
59
+ this.range = options.range;
60
+ this.children = options.children;
61
+ this.text = options.text;
62
+ this.bodyRange = options.bodyRange;
63
+ this.#documentSource = options.documentSource;
64
+ }
65
+ /** The section's source text, heading included. */
66
+ get source() {
67
+ return this.#documentSource.slice(this.range.offset, this.range.offset + this.range.length);
68
+ }
69
+ /**
70
+ * The section's source text without its heading.
71
+ *
72
+ * @remarks
73
+ * **Untrimmed, deliberately.** It is exactly the bytes {@link
74
+ * DocumentSection.bodyRange} describes, so the string and the offsets can
75
+ * never disagree — a package that publishes a span's offsets and then hands
76
+ * back a different span is lying about it. Callers that want the tidy form
77
+ * write `.body.trim()`, visibly.
78
+ */
79
+ get body() {
80
+ return this.#documentSource.slice(this.bodyRange.offset, this.bodyRange.offset + this.bodyRange.length);
81
+ }
82
+ };
9
83
  const walkTree = (node, depth, visit) => {
10
84
  if (depth > 256) throw new Error(`NestingDepthExceeded: limit ${256} exceeded while walking the document tree`);
11
85
  visit(node);
@@ -145,19 +219,68 @@ var MarkdownDocument = class MarkdownDocument extends Schema.Class("MarkdownDocu
145
219
  }
146
220
  }
147
221
  const start = block.position.start.offset;
148
- sections.push({
222
+ const bodyStart = block.position.end.offset;
223
+ sections.push(new DocumentSection({
149
224
  heading: block,
150
225
  depth: block.depth,
151
226
  range: MarkdownRange.make({
152
227
  offset: start,
153
228
  length: boundary - start
154
229
  }),
155
- children: blocks.slice(index + 1, end).filter((child) => child.type !== "frontmatter")
156
- });
230
+ children: blocks.slice(index + 1, end).filter((child) => child.type !== "frontmatter"),
231
+ text: phrasingText(block.children),
232
+ bodyRange: MarkdownRange.make({
233
+ offset: bodyStart,
234
+ length: Math.max(0, boundary - bodyStart)
235
+ }),
236
+ documentSource: this.source
237
+ }));
157
238
  }
158
239
  return sections;
159
240
  }
160
241
  /**
242
+ * The first section in document order, optionally restricted by heading
243
+ * depth.
244
+ *
245
+ * @remarks
246
+ * `firstSection({ depth: 2 })` is "the newest entry" for a changelog whose
247
+ * releases are H2s under an H1 title. Like the other navigation accessors
248
+ * this recomputes {@link MarkdownDocument.sections}; a caller scanning one
249
+ * document repeatedly should bind `document.sections` once instead.
250
+ *
251
+ * @param options - Depth restriction.
252
+ * @returns The first matching section, or `undefined`.
253
+ */
254
+ firstSection(options) {
255
+ const depth = options?.depth;
256
+ return this.sections.find((section) => depth === void 0 || section.depth === depth);
257
+ }
258
+ /**
259
+ * The first section whose heading matches, in document order.
260
+ *
261
+ * @remarks
262
+ * A **string matches the trimmed heading text exactly** — never a substring.
263
+ * That is a deliberate refusal of the obvious convenience: `"1.2.3"` would
264
+ * otherwise match a `## 1.2.30` heading, and in a changelog (where `1.2.30`
265
+ * sorts newer and therefore sits *earlier*) that silently publishes the
266
+ * wrong release. A `RegExp` is tested against the same trimmed text, and its
267
+ * `lastIndex` is reset first so a `/g/` pattern cannot skip sections. A
268
+ * predicate receives the raw {@link DocumentSection.text} plus the section
269
+ * itself and decides freely.
270
+ *
271
+ * @param match - A string, a RegExp, or a predicate.
272
+ * @param options - Depth restriction, applied before matching.
273
+ * @returns The first matching section, or `undefined`.
274
+ */
275
+ sectionByHeading(match, options) {
276
+ const depth = options?.depth;
277
+ const matches = typeof match === "string" ? (section) => section.text.trim() === match : match instanceof RegExp ? (section) => {
278
+ match.lastIndex = 0;
279
+ return match.test(section.text.trim());
280
+ } : (section) => match(section.text, section);
281
+ return this.sections.find((section) => (depth === void 0 || section.depth === depth) && matches(section));
282
+ }
283
+ /**
161
284
  * Every link-bearing node in the document, in document order: links,
162
285
  * images, definitions, and the reference forms resolved through the
163
286
  * definition index.
@@ -245,4 +368,4 @@ var MarkdownDocument = class MarkdownDocument extends Schema.Class("MarkdownDocu
245
368
  };
246
369
 
247
370
  //#endregion
248
- export { MarkdownDocument };
371
+ export { DocumentSection, MarkdownDocument };
package/index.d.ts CHANGED
@@ -1049,14 +1049,88 @@ interface DocumentHeading {
1049
1049
  * `children` include its subsections — the list is flat, in document order,
1050
1050
  * with `depth` carrying the hierarchy.
1051
1051
  *
1052
+ * Instances come from {@link MarkdownDocument.sections} and the two finders;
1053
+ * there is no reason to construct one directly.
1054
+ *
1052
1055
  * @public
1053
1056
  */
1054
- interface DocumentSection {
1057
+ declare class DocumentSection {
1058
+ #private;
1059
+ /** The heading that opens the section. */
1055
1060
  readonly heading: Heading;
1061
+ /** The heading's depth, and the section's. */
1056
1062
  readonly depth: HeadingDepth;
1063
+ /** The whole section including its heading — spliceable by `MarkdownEdit`. */
1057
1064
  readonly range: MarkdownRange;
1065
+ /** The root-level blocks after the heading, subsections included. */
1058
1066
  readonly children: ReadonlyArray<FlowContent>;
1067
+ /**
1068
+ * The heading's plain-text content.
1069
+ *
1070
+ * @remarks
1071
+ * The same derivation as {@link DocumentHeading.text}: text and inline-code
1072
+ * values concatenated, image `alt` where present, a hard break as one space,
1073
+ * nothing for raw HTML or footnote references. So `## **1.2.3**` reads
1074
+ * `1.2.3`.
1075
+ */
1076
+ readonly text: string;
1077
+ /**
1078
+ * The section WITHOUT its heading: from the heading's end offset to the
1079
+ * section's end.
1080
+ *
1081
+ * @remarks
1082
+ * This is the span a "give me the release notes under this version" reader
1083
+ * wants, and the one that is fiddly to derive by hand.
1084
+ */
1085
+ readonly bodyRange: MarkdownRange;
1086
+ constructor(options: {
1087
+ readonly heading: Heading;
1088
+ readonly depth: HeadingDepth;
1089
+ readonly range: MarkdownRange;
1090
+ readonly children: ReadonlyArray<FlowContent>;
1091
+ readonly text: string;
1092
+ readonly bodyRange: MarkdownRange;
1093
+ readonly documentSource: string;
1094
+ });
1095
+ /** The section's source text, heading included. */
1096
+ get source(): string;
1097
+ /**
1098
+ * The section's source text without its heading.
1099
+ *
1100
+ * @remarks
1101
+ * **Untrimmed, deliberately.** It is exactly the bytes {@link
1102
+ * DocumentSection.bodyRange} describes, so the string and the offsets can
1103
+ * never disagree — a package that publishes a span's offsets and then hands
1104
+ * back a different span is lying about it. Callers that want the tidy form
1105
+ * write `.body.trim()`, visibly.
1106
+ */
1107
+ get body(): string;
1059
1108
  }
1109
+ /**
1110
+ * Narrowing options for {@link MarkdownDocument.firstSection} and
1111
+ * {@link MarkdownDocument.sectionByHeading}.
1112
+ *
1113
+ * @public
1114
+ */
1115
+ interface SectionQueryOptions {
1116
+ /**
1117
+ * Restrict to sections opened by a heading of exactly this depth.
1118
+ *
1119
+ * @remarks
1120
+ * Equality, not a maximum: `{ depth: 2 }` is "the H2 sections", which is the
1121
+ * changelog idiom. A depth *range* is a different question and belongs in a
1122
+ * predicate.
1123
+ */
1124
+ readonly depth?: HeadingDepth;
1125
+ }
1126
+ /**
1127
+ * How {@link MarkdownDocument.sectionByHeading} decides a section matches: an
1128
+ * exact trimmed-text string, a `RegExp`, or a predicate over the text and the
1129
+ * section.
1130
+ *
1131
+ * @public
1132
+ */
1133
+ type SectionHeadingMatch = string | RegExp | ((text: string, section: DocumentSection) => boolean);
1060
1134
  /**
1061
1135
  * The node types {@link MarkdownDocument.links} collects: the nodes that
1062
1136
  * carry an outbound URL themselves ({@link Link}, {@link Image},
@@ -1154,6 +1228,38 @@ declare class MarkdownDocument extends MarkdownDocument_base {
1154
1228
  * boundary heading's start, or to the end of the source.
1155
1229
  */
1156
1230
  get sections(): ReadonlyArray<DocumentSection>;
1231
+ /**
1232
+ * The first section in document order, optionally restricted by heading
1233
+ * depth.
1234
+ *
1235
+ * @remarks
1236
+ * `firstSection({ depth: 2 })` is "the newest entry" for a changelog whose
1237
+ * releases are H2s under an H1 title. Like the other navigation accessors
1238
+ * this recomputes {@link MarkdownDocument.sections}; a caller scanning one
1239
+ * document repeatedly should bind `document.sections` once instead.
1240
+ *
1241
+ * @param options - Depth restriction.
1242
+ * @returns The first matching section, or `undefined`.
1243
+ */
1244
+ firstSection(options?: SectionQueryOptions): DocumentSection | undefined;
1245
+ /**
1246
+ * The first section whose heading matches, in document order.
1247
+ *
1248
+ * @remarks
1249
+ * A **string matches the trimmed heading text exactly** — never a substring.
1250
+ * That is a deliberate refusal of the obvious convenience: `"1.2.3"` would
1251
+ * otherwise match a `## 1.2.30` heading, and in a changelog (where `1.2.30`
1252
+ * sorts newer and therefore sits *earlier*) that silently publishes the
1253
+ * wrong release. A `RegExp` is tested against the same trimmed text, and its
1254
+ * `lastIndex` is reset first so a `/g/` pattern cannot skip sections. A
1255
+ * predicate receives the raw {@link DocumentSection.text} plus the section
1256
+ * itself and decides freely.
1257
+ *
1258
+ * @param match - A string, a RegExp, or a predicate.
1259
+ * @param options - Depth restriction, applied before matching.
1260
+ * @returns The first matching section, or `undefined`.
1261
+ */
1262
+ sectionByHeading(match: SectionHeadingMatch, options?: SectionQueryOptions): DocumentSection | undefined;
1157
1263
  /**
1158
1264
  * Every link-bearing node in the document, in document order: links,
1159
1265
  * images, definitions, and the reference forms resolved through the
@@ -2186,5 +2292,5 @@ declare const TomlFrontmatter: FrontmatterCodec;
2186
2292
  */
2187
2293
  declare const YamlFrontmatter: FrontmatterCodec;
2188
2294
  //#endregion
2189
- export { Blockquote, Break, BreakStyle, BulletChar, Code, Definition, Delete, type DocumentHeading, type DocumentLink, type DocumentSection, Emphasis, EmphasisChar, FenceChar, FlowContent, FootnoteDefinition, FootnoteReference, Frontmatter, type FrontmatterCodec, FrontmatterContent, FrontmatterDecodeError, FrontmatterEncodeError, FrontmatterFormat, FrontmatterFormatMismatchError, FrontmatterMissingError, type FrontmatterResolveError, type FrontmatterSchemaError, type FrontmatterSchemaResolver, FrontmatterValidationError, type FrontmatterWriteError, Heading, HeadingDepth, HeadingStyle, Html, Image, ImageReference, InlineCode, JsonFrontmatter, Link, type LinkBearingNode, LinkReference, List, ListContent, ListDelimiter, ListItem, Markdown, MarkdownDiagnostic, MarkdownDialect, MarkdownDocument, MarkdownEdit, MarkdownFormat, MarkdownFormattingOptions, MarkdownFrontmatter, MarkdownModificationError, MarkdownModificationErrorCode, MarkdownNode, type MarkdownNodeOfType, type MarkdownNodeType, MarkdownParseError, MarkdownParseErrorCode, MarkdownParseOptions, type MarkdownPath, MarkdownRange, type MarkdownRangeLike, type MarkdownSegment, MarkdownStringifyError, MarkdownVisitor, MarkdownVisitorEvent, Mdast, MdastDecodeError, type MdastNode, Paragraph, PhrasingContent, Point, Position, ReferenceType, Root, RowContent, SchemaDeclaration, SchemaDeclarationByName, SchemaDeclarationByPath, SchemaDeclarationByUrl, SchemaDeclarationInline, SchemaDeclarationInvalidError, SchemaDeclarationMissingError, SchemaNameUnknownError, SchemaResolver, SchemaVersionUnresolvableError, Strong, Table, TableAlign, TableCell, TableContent, TableRow, Text, ThematicBreak, ThematicBreakChar, TomlFrontmatter, YamlFrontmatter };
2295
+ export { Blockquote, Break, BreakStyle, BulletChar, Code, Definition, Delete, type DocumentHeading, type DocumentLink, DocumentSection, Emphasis, EmphasisChar, FenceChar, FlowContent, FootnoteDefinition, FootnoteReference, Frontmatter, type FrontmatterCodec, FrontmatterContent, FrontmatterDecodeError, FrontmatterEncodeError, FrontmatterFormat, FrontmatterFormatMismatchError, FrontmatterMissingError, type FrontmatterResolveError, type FrontmatterSchemaError, type FrontmatterSchemaResolver, FrontmatterValidationError, type FrontmatterWriteError, Heading, HeadingDepth, HeadingStyle, Html, Image, ImageReference, InlineCode, JsonFrontmatter, Link, type LinkBearingNode, LinkReference, List, ListContent, ListDelimiter, ListItem, Markdown, MarkdownDiagnostic, MarkdownDialect, MarkdownDocument, MarkdownEdit, MarkdownFormat, MarkdownFormattingOptions, MarkdownFrontmatter, MarkdownModificationError, MarkdownModificationErrorCode, MarkdownNode, type MarkdownNodeOfType, type MarkdownNodeType, MarkdownParseError, MarkdownParseErrorCode, MarkdownParseOptions, type MarkdownPath, MarkdownRange, type MarkdownRangeLike, type MarkdownSegment, MarkdownStringifyError, MarkdownVisitor, MarkdownVisitorEvent, Mdast, MdastDecodeError, type MdastNode, Paragraph, PhrasingContent, Point, Position, ReferenceType, Root, RowContent, SchemaDeclaration, SchemaDeclarationByName, SchemaDeclarationByPath, SchemaDeclarationByUrl, SchemaDeclarationInline, SchemaDeclarationInvalidError, SchemaDeclarationMissingError, SchemaNameUnknownError, SchemaResolver, SchemaVersionUnresolvableError, type SectionHeadingMatch, type SectionQueryOptions, Strong, Table, TableAlign, TableCell, TableContent, TableRow, Text, ThematicBreak, ThematicBreakChar, TomlFrontmatter, YamlFrontmatter };
2190
2296
  //# sourceMappingURL=index.d.ts.map
package/index.js CHANGED
@@ -5,11 +5,11 @@ import { SchemaDeclaration, SchemaDeclarationByName, SchemaDeclarationByPath, Sc
5
5
  import { JsonFrontmatter } from "./JsonFrontmatter.js";
6
6
  import { MarkdownDiagnostic, MarkdownParseErrorCode } from "./MarkdownDiagnostic.js";
7
7
  import { Markdown, MarkdownDialect, MarkdownParseError, MarkdownParseOptions, MarkdownStringifyError } from "./Markdown.js";
8
- import { MarkdownDocument } from "./MarkdownDocument.js";
8
+ import { DocumentSection, MarkdownDocument } from "./MarkdownDocument.js";
9
9
  import { MarkdownFormat, MarkdownFormattingOptions, MarkdownModificationError, MarkdownModificationErrorCode } from "./MarkdownFormat.js";
10
10
  import { MarkdownVisitor, MarkdownVisitorEvent } from "./MarkdownVisitor.js";
11
11
  import { Mdast, MdastDecodeError } from "./Mdast.js";
12
12
  import { TomlFrontmatter } from "./TomlFrontmatter.js";
13
13
  import { YamlFrontmatter } from "./YamlFrontmatter.js";
14
14
 
15
- export { Blockquote, Break, BreakStyle, BulletChar, Code, Definition, Delete, Emphasis, EmphasisChar, FenceChar, FlowContent, FootnoteDefinition, FootnoteReference, Frontmatter, FrontmatterContent, FrontmatterDecodeError, FrontmatterEncodeError, FrontmatterFormat, FrontmatterFormatMismatchError, FrontmatterMissingError, FrontmatterValidationError, Heading, HeadingDepth, HeadingStyle, Html, Image, ImageReference, InlineCode, JsonFrontmatter, Link, LinkReference, List, ListContent, ListDelimiter, ListItem, Markdown, MarkdownDiagnostic, MarkdownDialect, MarkdownDocument, MarkdownEdit, MarkdownFormat, MarkdownFormattingOptions, MarkdownFrontmatter, MarkdownModificationError, MarkdownModificationErrorCode, MarkdownNode, MarkdownParseError, MarkdownParseErrorCode, MarkdownParseOptions, MarkdownRange, MarkdownStringifyError, MarkdownVisitor, MarkdownVisitorEvent, Mdast, MdastDecodeError, Paragraph, PhrasingContent, Point, Position, ReferenceType, Root, RowContent, SchemaDeclaration, SchemaDeclarationByName, SchemaDeclarationByPath, SchemaDeclarationByUrl, SchemaDeclarationInline, SchemaDeclarationInvalidError, SchemaDeclarationMissingError, SchemaNameUnknownError, SchemaResolver, SchemaVersionUnresolvableError, Strong, Table, TableAlign, TableCell, TableContent, TableRow, Text, ThematicBreak, ThematicBreakChar, TomlFrontmatter, YamlFrontmatter };
15
+ export { Blockquote, Break, BreakStyle, BulletChar, Code, Definition, Delete, DocumentSection, Emphasis, EmphasisChar, FenceChar, FlowContent, FootnoteDefinition, FootnoteReference, Frontmatter, FrontmatterContent, FrontmatterDecodeError, FrontmatterEncodeError, FrontmatterFormat, FrontmatterFormatMismatchError, FrontmatterMissingError, FrontmatterValidationError, Heading, HeadingDepth, HeadingStyle, Html, Image, ImageReference, InlineCode, JsonFrontmatter, Link, LinkReference, List, ListContent, ListDelimiter, ListItem, Markdown, MarkdownDiagnostic, MarkdownDialect, MarkdownDocument, MarkdownEdit, MarkdownFormat, MarkdownFormattingOptions, MarkdownFrontmatter, MarkdownModificationError, MarkdownModificationErrorCode, MarkdownNode, MarkdownParseError, MarkdownParseErrorCode, MarkdownParseOptions, MarkdownRange, MarkdownStringifyError, MarkdownVisitor, MarkdownVisitorEvent, Mdast, MdastDecodeError, Paragraph, PhrasingContent, Point, Position, ReferenceType, Root, RowContent, SchemaDeclaration, SchemaDeclarationByName, SchemaDeclarationByPath, SchemaDeclarationByUrl, SchemaDeclarationInline, SchemaDeclarationInvalidError, SchemaDeclarationMissingError, SchemaNameUnknownError, SchemaResolver, SchemaVersionUnresolvableError, Strong, Table, TableAlign, TableCell, TableContent, TableRow, Text, ThematicBreak, ThematicBreakChar, TomlFrontmatter, YamlFrontmatter };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@effected/markdown",
3
- "version": "0.2.5",
3
+ "version": "0.3.0",
4
4
  "private": false,
5
5
  "description": "CommonMark and GFM markdown parse, edit and transform schemas for Effect",
6
6
  "keywords": [