Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
38 changes: 33 additions & 5 deletions docs/design/api-mark-dot-net/dot-net-emitter-gradual-disclosure.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,13 @@ single namespace.
- *Returns*: `void`
- *Algorithm*: Creates the type page writer internally via `ctx.Factory.CreateMarkdown`;
writes an H1 heading with the type's simple name; emits the C# signature
via `writer.WriteSignature("csharp", ...)`, which produces a fenced C# code block; emits the XML summary and remarks paragraphs; emits structured example blocks via `ctx.XmlDocs.GetExampleParts(typeMemberId)`; groups all
via `writer.WriteSignature("csharp", ...)`, which produces a fenced C# code block; fetches the
XML summary via `GetSummaryMarkdown` (multi-line rendering) and the remarks text
before deciding what to write: when a summary is present it is emitted as a
paragraph; otherwise the "No description provided." placeholder is emitted ONLY
when remarks are also absent (see "Placeholder Suppression" below) — when remarks
alone are present, nothing is written here and the remarks paragraph (emitted next)
carries the type's description; emits structured example blocks via `ctx.XmlDocs.GetExampleParts(typeMemberId)`; groups all
visible members by kind (Constructors, Properties, Fields, Events, Methods,
Operators, Nested Types) and writes one table row per member (or one representative row
per method overload group) with a link to the member's dedicated page; calls per-kind page writers for each member or member group.
Expand All @@ -80,8 +86,11 @@ for a single method (or one overload) onto the caller-supplied writer.
XML-doc member ID used for lookup; `MethodDocContext context` — namespace name,
XML doc reader, resolver, current folder, and external type accumulator.
- *Returns*: `void`
- *Algorithm*: Emits the C# method signature via `writer.WriteSignature("csharp", ...)`, which produces a fenced C# code block; writes the XML
summary as a paragraph (or the placeholder when absent); if the method has parameters,
- *Algorithm*: Emits the C# method signature via `writer.WriteSignature("csharp", ...)`, which produces a fenced C# code block; fetches the XML
summary via `GetSummaryMarkdown` and the remarks text before deciding what to
write: a present summary is written as a paragraph; otherwise the "No
description provided." placeholder is written only when remarks are also
absent (see "Placeholder Suppression" below); if the method has parameters,
writes a parameter table with Parameter, Type, and Description columns — type cells are
resolved via `resolver.Linkify` and external types are accumulated into
`context.ExternalTypes`; if a returns value is documented, writes a `**Returns:**`
Expand Down Expand Up @@ -208,11 +217,30 @@ non-method member (property, field, or event) to the supplied Markdown writer.
`string memberId`, `MethodDocContext ctx` — provides namespace name, XML docs,
resolver, current folder, and external type accumulator.
- *Returns*: `void`
- *Algorithm*: Emits the C# member signature via `writer.WriteSignature("csharp", ...)`, which produces a fenced C# code block; writes the XML
XML summary as a paragraph (or the placeholder when absent); emits returns,
- *Algorithm*: Emits the C# member signature via `writer.WriteSignature("csharp", ...)`, which produces a fenced C# code block; fetches the XML
summary via `GetSummaryMarkdown` and the remarks text before deciding what to
write (same placeholder-suppression rule as `WriteMethodDocumentation` — see
"Placeholder Suppression" below); emits returns,
exceptions, remarks, and example sections when present, following the same
pattern as `WriteMethodDocumentation` for non-method member kinds.

#### Placeholder Suppression

`WriteTypeHeaderSections`, `WriteNonMethodMemberContent`, and
`WriteMethodDocumentation` each fetch the member's (or type's) remarks text
*before* deciding whether to emit `DotNetEmitter.NoDescriptionPlaceholder`
("No description provided."): the placeholder is written only when BOTH the
summary AND the remarks are absent. When a summary is absent but remarks
content is present, nothing is written in the summary's place — the remarks
paragraph, emitted immediately afterward, carries the description instead.

This scoping applies ONLY to member-detail-page and type-page bodies, which
render both summary and remarks. Table-cell summary fallback (the nested-types
table, per-kind member-row tables, and overload-group tables on a type page)
deliberately keeps the unconditional placeholder-on-empty-summary behavior
unchanged, since table cells never render remarks — the placeholder remains
the only signal available in that compact context.

### Path Conventions

The assembly index page (`api.md`) also writes a `## File Naming and Path Convention`
Expand Down
17 changes: 16 additions & 1 deletion docs/design/api-mark-dot-net/dot-net-emitter-single-file.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,12 @@ bullet list, and then dispatches to `WriteSingleFileMemberSection` for each
visible member. Recursively calls `WriteSingleFileNestedTypes` when the type
contains nested types.

- The summary is fetched via `GetSummaryMarkdown` (multi-line rendering) and the
remarks text is fetched before deciding what to write: a present summary is
written as a paragraph; otherwise the "No description provided." placeholder
is written only when remarks are also absent — matching the same
placeholder-suppression rule as `DotNetEmitterGradualDisclosure`'s
member-detail-page bodies (see that unit's design doc for the full rationale).
- For nested types, a notice paragraph `"Nested type of \`{OuterType}\`."` is
emitted immediately after the H{depth+2} heading to establish parent context.
- When the type is a delegate (detected via `DotNetEmitter.IsDelegate`), the
Expand All @@ -66,7 +72,16 @@ contains nested types.

**WriteSingleFileMemberSection** (private): Writes the full per-member block for
one member — member heading, signature code block, summary, parameter table,
returns documentation, exception table, and example block. A throw-away empty
returns documentation, exception table, and example block. The summary is
fetched via `GetSummaryMarkdown` (multi-line rendering), so a `<list>` inside a
member's `<summary>` renders as real multi-line Markdown here too. Remarks text
is fetched before deciding what to write, mirroring `WriteSingleFileTypeSections`:
a present summary is written as a paragraph; otherwise the "No description
provided." placeholder is written only when remarks are also absent. When
present, remarks are written as a separate paragraph after the summary (or
placeholder) — the same placeholder-suppression rule described for
`WriteSingleFileTypeSections` applies identically at the member level. A
throw-away empty
`SortedSet<ExternalTypeInfo>` is passed to each `resolver.Linkify` call; because
`generateLinks` is `false` and no External Types section is emitted in single-file
output, this set is never populated or read. Note that `namespaceFolderPath` is
Expand Down
31 changes: 31 additions & 0 deletions docs/design/api-mark-dot-net/type-link-resolver.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,37 @@ reference collected during table cell generation.
8. Non-System external → track in `externalTypes`, return plain name.
- *Intra-assembly detection*: `TypeReference.Scope is ModuleDefinition`.

**TypeLinkResolver.GetTypePageKey** (private): Computes the documentation page
key (relative path without extension) for an intra-assembly `TypeReference`,
correctly handling nested types of arbitrary nesting depth.

- *Problem*: Mono.Cecil gives nested types an **empty** `TypeReference.Namespace`
— the namespace is carried only by the outermost non-nested ancestor. Using
`typeRef.Namespace` directly for a nested type therefore produced an empty
namespace folder and a broken link, regardless of whether the nested type was
referenced as a method parameter, return type, field type, or generic type
argument.
- *Algorithm*:
1. Walk `TypeReference.DeclaringType` from `typeRef` upward, collecting every
ancestor (including `typeRef` itself) into a list ordered outermost to
innermost.
2. Derive the namespace folder via `DotNetEmitter.GetNamespaceFolderPath` using
the **outermost** ancestor's `Namespace` — the only member of the chain
guaranteed to carry a non-empty namespace.
3. Join every ancestor's name, outermost to innermost, each flattened via
`TypeNameSimplifier.FlattenArity`, with `/` separators.
4. Prefix the joined path with the namespace folder (when non-empty) to
produce the final page key, e.g. `MyLib/Outer/Inner` or, for two levels of
nesting, `MyLib/Outer/Middle/Inner`.
- *Non-nested types*: For a non-nested type, the chain has exactly one element,
so the algorithm naturally reduces to the pre-existing non-nested behavior
with no special-casing required.
- *Consistency*: This mirrors exactly how
`DotNetEmitterGradualDisclosure.WriteNestedTypesSection` writes nested type
pages to disk (`{NamespaceFolderPath}/{ParentTypeName}/{NestedTypeName}`,
recursively for arbitrary nesting depth), so every link produced by
`GetTypePageKey` resolves to a page that the emitter actually creates.

### Error Handling

`Linkify` returns `string.Empty` for a null `typeRef` rather than throwing.
Expand Down
80 changes: 77 additions & 3 deletions docs/design/api-mark-dot-net/xml-doc-reader.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,22 @@ member index.

**GetSummary**: Returns trimmed single-line summary text for `memberId`, or
`null` if absent. Summary text is normalized to a single line because summaries
are by convention brief one-liner descriptions. Resolves `<inheritdoc />` first.
are by convention brief one-liner descriptions, and this method feeds compact
contexts such as table cells and bullet quick-index lines. Resolves
`<inheritdoc />` first. When the summary contains a `<list>` element (bullet,
number, or table), the list is rendered inline using single-line numbered
markers (`(1) item one (2) item two`) via `AppendInlineMarkerList` rather than
losing item separation — see "Single-line vs. multi-line list rendering" below.

**GetSummaryMarkdown**: Returns the full multi-line Markdown rendering of the
`<summary>` element for `memberId`, or `null` if absent. Unlike `GetSummary`,
this preserves real block structure: a `<list>` element renders as an actual
multi-line Markdown list/table (blank line before the list, each item on its
own line, numbered markers for `type="number"`), with any trailing prose after
`</list>` rendered as its own paragraph — exactly the same multi-line rendering
already used by `GetRemarks`. Intended for member-detail-page and type-page
bodies, which have room for full Markdown, as opposed to `GetSummary`'s
single-line contract for table cells. Resolves `<inheritdoc />` first.

**GetRemarks**: Returns trimmed remarks text for `memberId`, or `null` if
absent. May contain multiple lines. Resolves `<inheritdoc />` first.
Expand Down Expand Up @@ -149,7 +164,38 @@ for a given ID by following `<inheritdoc />` recursively with cycle detection.
**Whitespace normalization**: `GetDocumentationText` normalizes text by
collapsing internal whitespace within each line. `GetSingleLineDocumentationText`
additionally joins all non-empty trimmed lines into a single space-separated
string. Both normalize line endings to `\n` before processing.
string. Both normalize line endings to `\n` before processing. Both pass an
explicit `singleLine` flag down through `AppendNodeText`/`AppendElementText`/
`AppendListText` so that list rendering (see "Single-line vs. multi-line list
rendering" below) can tell which of the two contexts it is being called from.

#### Single-line vs. multi-line list rendering

`AppendNodeText`, `AppendElementText`, and `AppendListText` each accept a
`bool singleLine` parameter that determines how a `<list>` element renders:

- `singleLine: false` (used by `GetDocumentationText`, i.e. `GetRemarks`,
`GetExample`/`GetExampleParts`, and the new `GetSummaryMarkdown`) — renders
real multi-line Markdown exactly as described in the `<list>` bullet under
"Inline Element Rendering" below: a blank line before the list, each item on
its own line (dash, numbered, or table-row), and any trailing prose as a
separate paragraph.
- `singleLine: true` (used by `GetSingleLineDocumentationText`, i.e.
`GetSummary`) — delegates to `AppendInlineMarkerList`, which renders every
list `type` variant (bullet, number, table) uniformly as single-line numbered
markers: `(1) item one (2) item two (3) item three`, joined with a single
space. None of the three list types can express genuine block structure on
one line, so numbering is used consistently across all of them purely to
preserve a readable item boundary.

This flag threads through unchanged from the top-level entry point down to
every recursive call, with one exception: nested inline content inside a
`<term>`/`<description>` (used by `type="table"` list items, via
`RenderTermDescription`/`RenderInlineElement`) is always rendered through the
`singleLine: false` branch and then collapsed with `NormalizeSingleLine`,
regardless of which context invoked the outer list. This produces the same
visible single-line result as genuine flag propagation would, but the
`singleLine` value itself is not passed into that nested call.

#### Inline Element Rendering

Expand Down Expand Up @@ -209,7 +255,35 @@ display string.
declaring type name for constructor crefs; appends `()` to method crefs that
include a parameter list.

**Code block dedentation** (`DedentCode`, private static): Removes common leading
**FormatMemberReference** (private): Formats a `<see cref>` reference whose
kind indicates a type member (`M:`, `P:`, `F:`, or `E:`) as `Type.Member` text,
used by `FormatCref` once the kind-prefix dispatch determines the cref targets
a member rather than a type.

- *Parameters*: `char kind` — the single-character cref kind prefix (`M`, `P`,
`F`, or `E`); `string target` — the qualified type-and-member portion of the
cref value; `string parameters` — the method parameter list text (methods
only; empty for properties/fields/events).
- *Algorithm*: Splits `target` into a declaring type name and a member name,
formats each individually, and always renders `{formattedTypeName}.{formattedMemberName}`
for every one of M/P/F/E — the declaring type name is never omitted. Method
crefs additionally append the formatted parameter list.
- *History*: Prior to this behavior, the declaring type name was included only
for `M:` (method) crefs, or when the declaring type happened to be a C#
primitive; a `P:`/`F:`/`E:` cref to a non-primitive type rendered as a bare
member name with no indication of which type it belonged to — ambiguous
whenever multiple types in scope share a member name. The now-removed special
case was not an intentional, documented design decision (confirmed via `git
blame` and the absence of any test asserting the omitted-type-name behavior
by name), so it was corrected to match the already-correct `M:` behavior for
all four member kinds.
- *Scope note*: This fix intentionally stops at rendering the declaring type
name as plain text. It does NOT attempt to turn the reference into a link to
the member's own page — `XmlDocReader` has no access to `TypeLinkResolver` or
any other page-routing context, so making the reference an actual
cross-reference link would require an architectural change beyond this
method's scope.

indentation from raw `<code>` element content so the result renders flush-left in
a fenced Markdown code block. The minimum indentation is computed from the leading
whitespace of every non-blank line (blank lines are excluded from the calculation
Expand Down
6 changes: 6 additions & 0 deletions docs/reqstream/api-mark-dot-net.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -52,11 +52,13 @@ sections:
- ApiMarkDotNet-DotNetEmitterGradualDisclosure-EmitNestedTypePages
- ApiMarkDotNet-DotNetEmitterGradualDisclosure-EmitOperatorsSummaryDocumentation
- ApiMarkDotNet-DotNetEmitterGradualDisclosure-EmitAssemblyDescription
- ApiMarkDotNet-DotNetEmitterGradualDisclosure-SuppressPlaceholderWhenRemarksPresent
- ApiMarkDotNet-DotNetEmitterSingleFile-EmitAllApiIntoSingleFile
- ApiMarkDotNet-DotNetEmitterSingleFile-OffsetHeadingsByHeadingDepth
- ApiMarkDotNet-DotNetEmitterSingleFile-EmitAssemblyDescription
- ApiMarkDotNet-DotNetEmitterSingleFile-EmitNamespaceDocSummary
- ApiMarkDotNet-DotNetEmitterSingleFile-EmitNamespaceDocRemarksAndExamples
- ApiMarkDotNet-DotNetEmitterSingleFile-SuppressPlaceholderWhenRemarksPresent
- ApiMarkDotNet-DotNetEmitterSingleFile-EmitCompactMemberBulletList
- ApiMarkDotNet-DotNetEmitterSingleFile-EmitMembersConstructorsFirst
- ApiMarkDotNet-DotNetEmitterSingleFile-SkipMemberListForDelegates
Expand All @@ -68,9 +70,13 @@ sections:
- ApiMarkDotNet-TypeLinkResolver-RenderArrayTypesWithSuffix
- ApiMarkDotNet-TypeLinkResolver-RenderGenericTypesWithTypeArguments
- ApiMarkDotNet-TypeLinkResolver-UnwrapNullableValueTypes
- ApiMarkDotNet-TypeLinkResolver-ResolveNestedTypesToCorrectPageKey
- ApiMarkDotNet-XmlDocReader-ProvideMemberDocumentationFromXmlFile
- ApiMarkDotNet-XmlDocReader-RenderXmlDocLists
- ApiMarkDotNet-XmlDocReader-InheritDoc
- ApiMarkDotNet-XmlDocReader-RenderSummaryListsAsMultiLineMarkdown
- ApiMarkDotNet-XmlDocReader-RenderSummaryListsAsInlineMarkersWhenSingleLine
- ApiMarkDotNet-XmlDocReader-IncludeTypeNameForPropertyFieldEventCrefs
- ApiMarkDotNet-ExternalXmlDocResolver-LocateAndCacheReferencedAssemblyDocs
- ApiMarkDotNet-ExternalXmlDocResolver-RejectNullReferencePaths
- ApiMarkDotNet-ExternalXmlDocResolver-ProbeHintedAssemblyBeforeFullScan
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,3 +130,18 @@ sections:
who only need the operator API without navigating to other pages.
tests:
- DotNetGenerator_Generate_TypeWithOperators_OperatorsPageContainsSummaries
- id: ApiMarkDotNet-DotNetEmitterGradualDisclosure-SuppressPlaceholderWhenRemarksPresent
title: DotNetEmitterGradualDisclosure shall suppress the "No description
provided." placeholder on member-detail-page and type-page bodies when
a summary is absent but remarks content is present, showing the
placeholder only when both summary and remarks are absent.
justification: |
Member-detail-page and type-page bodies render both summary and remarks,
so showing the no-description placeholder whenever a summary was absent
— even though valid remarks content followed immediately below it — was
misleading: it suggested the member was undocumented when it was not.
Table-cell summary fallback (which never shows remarks) is unaffected,
since the placeholder remains the only signal available there.
tests:
- DotNetEmitterGradualDisclosure_Emit_MemberWithRemarksOnly_SuppressesPlaceholderAndShowsRemarks
- DotNetEmitterGradualDisclosure_Emit_MemberWithNeitherSummaryNorRemarks_StillShowsPlaceholder
Loading
Loading