Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),

## [Unreleased]

### Added

- Link YARD-style `@see` references and URLs in generated Markdown, including cross-file CLI references and equivalent `.#` and `?.` module-function spellings.

## [0.0.1] - 2026-10-11

The first ZARD release separates Ruby API documentation from RBS and Rigor contracts. Parse Ruby source into a versioned model, generate public API Markdown, and lint documentation from files, directories, or standard input without installing Rigor.
Expand Down
1 change: 1 addition & 0 deletions docs/adr/0002-em-dash-zard-documentation-marker.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ ZARD uses an em dash `—` when a named documentation tag or an optional type mu
- `@option options :format — Output format.` is ZARD documentation.
- `@option options :format [Symbol] — Output format.` is ZARD documentation with a claim.
- Tags with no name and no type, such as `@note`, `@see`, `@deprecated`, and `@example`, write their description directly.
- For YARD compatibility, `@see` descriptions may contain a reference followed by a whitespace-separated display label; this form does not require `—`.
- `@param value [String] Input text.` is preserved as raw text, not as a type hint.
- Named tags such as `@param`, `@option`, `@yieldparam`, and `@raise` require `—` before their description.
- `zard-doc lint` warns about YARD-like tags and can promote the warning to an error.
Expand Down
112 changes: 112 additions & 0 deletions docs/adr/0004-documentation-reference-syntax.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
# Use BitClust references and YARD-compatible see tags

ZARD adopts BitClust Markdown references, accepts their RD link forms for compatibility, and supports YARD-style shorthand in `@see`. All forms share reference resolution, while original comment text and source provenance remain available. Compatibility makes existing Ruby documentation familiar and avoids a separate ZARD spelling for each kind of method.

Status: Accepted. The current implementation covers shorthand `@see` references and URLs; explicit BitClust and RD forms are specified here for subsequent implementation. This ADR records the language decision, not a claim that every form is already shipped.

## Problem Statement

Authors need links between Ruby declarations in API documentation. BitClust and YARD provide familiar conventions, but differ in delimiters, labels, and module-function spelling. Documentation must retain its original text even when a reference cannot be linked.

## Solution

Use explicit BitClust forms in prose and allow concise YARD-compatible targets in `@see`. Accept both `Kernel.#puts` and `Kernel?.puts` with identical meaning. Generated documentation links only to destinations it can identify reliably.

## User Stories

1. As an author, I want to link an instance method, so that readers can find related behavior.
2. As an author, I want to link a class method, so that readers can find construction and utility APIs.
3. As an author, I want either module-function spelling to work, so that existing comments remain usable.
4. As an author, I want relative method targets, so that comments need not repeat their owner.
5. As an author, I want an absolute target, so that a local namespace cannot change its meaning.
6. As an author, I want a custom label, so that references fit the surrounding explanation.
7. As an author, I want RD links accepted, so that migrating documentation does not require rewriting every reference.
8. As an author, I want links in prose and documentation tags, so that related APIs can be mentioned where they matter.
9. As an author, I want code examples preserved, so that literal reference syntax is not expanded inside code.
10. As a reader, I want links across generated input files, so that related declarations remain reachable.
11. As a reader, I want unresolved references preserved, so that incomplete documentation retains useful context.
12. As a tool author, I want original comments and source spans, so that normalization does not erase provenance.

## Implementation Decisions

### Accepted forms

| Form | Meaning | Availability |
| --- | --- | --- |
| `[m:Foo#bar]` | Explicit instance-method reference | Planned |
| `[m:Foo.bar]` | Explicit class-method reference | Planned |
| `[m:Kernel?.puts]` | Explicit module-function reference | Planned |
| `[label](m:Foo#bar)` | Explicit reference with a display label | Planned |
| `[c:Foo]` | Class or module reference | Planned |
| `[m:Math::PI]` | Constant reference | Planned |
| `[[m:Foo#bar]]` | RD-compatible method reference | Planned |
| `[[m:Kernel.#puts]]` | RD-compatible module-function reference | Planned |
| `[[c:Foo]]` | RD-compatible class or module reference | Planned |
| `@see Foo#bar` | Shorthand instance-method reference | Implemented |
| `@see Foo` / `@see Foo::CONST` | Shorthand class, module, or constant reference | Implemented |
| `@see Foo.bar Label` | Shorthand class-method reference with a label | Implemented |
| `@see #bar` / `@see .bar` | Reference relative to the current owner | Implemented |
| `@see Kernel.#puts` / `@see Kernel?.puts` | Equivalent module-function references | Implemented |
| `@see https://example.com Label` | External URL with an optional label | Implemented |

The `@see` examples are comment payloads, written as `# @see ...` in Ruby source. Explicit forms also belong in `@see` descriptions once implemented; they are not shorthand target tokens and must be recognized before shorthand parsing.

### Targets and normalization

- `Foo#bar` selects an instance method. `Foo.bar` selects a singleton method.
- `Kernel.#puts` and `Kernel?.puts` select the same module-function reference and link to its public singleton copy. Normalization preserves the original spelling for display and provenance. Relative `.#puts` and `?.puts` follow the same rule.
- A leading `::` requests a root target. Relative `#bar` and `.bar` use the current class or module; shorthand top-level instance methods use the empty owner.
- Qualified targets search the current namespace and its namespace prefixes. This matches YARD's namespace-based convention, including qualified openings such as `class Outer::Caller`; it does not reproduce Ruby's lexical constant lookup.
- A nearer declared owner shadows outer owners. If its requested member is missing, private, or undocumented, resolution stops and preserves the reference as text.
- Attribute writers use their actual method name with `=`. An accessor exposes reader and writer references, which may share one rendered declaration. A replacement reader remains distinct from a retained writer.
- Valid Unicode owners and method names participate in lookup. Method punctuation remains part of the target; normalization changes only the owner/method separator.
- Explicit constant receiver paths are matched against declared namespace candidates. Dynamic receiver expressions and runtime aliases do not establish object identity.

### Labels and comment boundaries

- Shorthand `@see` uses the first whitespace-separated token as its target and the remaining text as an optional display label. Leading and trailing whitespace is trimmed for rendering. No `—` is required, as clarified by ADR 0002.
- If no label is supplied, the original target spelling supplies the display text. Labels and destinations must be escaped appropriately for generated Markdown.
- Existing documentation continuation rules apply. Continuation lines remain part of the same `@see` description and, for a resolved shorthand reference, its label.
- Explicit Markdown forms use `[label](m:target)` for labels. A label consisting entirely of a backtick code span keeps that code presentation, following BitClust.
- Method names containing brackets use BitClust escaping: `[m:Hash#\[\]]`. RD compatibility accepts the historical form `[[m:Hash#[] ]]`, including its delimiter-separating space.
- References are recognized in prose, including documentation tag descriptions, while inline code and fenced or indented code blocks remain literal. An `@example` code region is not implicitly a reference region.
- RD compatibility covers link syntax only. It does not reinterpret the entire comment as an RD document.

### Resolution and output

- Resolve against explicit declarations in the generated input set. The single-document API uses that document; the CLI uses all input files in its combined output.
- A destination must identify exactly one public declaration with rendered API documentation. Private, undocumented, ambiguous, unresolved, and refinement targets remain text with their original description intact.
- Equivalent forms resolve to the same destination. References to a shared declaration use one stable, collision-free anchor, independent of parameter display and heading slug rules.
- URL shorthand supports HTTP, HTTPS, and mailto. It does not require declaration lookup.
- Declaration lookup does not follow inheritance or mixins, evaluate Ruby, infer runtime receiver identity, or fetch external API inventories. These limits are deliberate even where YARD can resolve more broadly.
- The core retains documentation descriptions, raw source, and spans. Reference interpretation belongs to documentation generation and does not turn a reference into a contract or documentation claim.
- A future structured reference representation must preserve provenance and respect the versioned model boundary; this ADR does not add a public model field or resolver API.

## Testing Decisions

Use the existing parse API, Markdown generation API, and CLI as the acceptance seams. Assert rendered destinations and preserved source text rather than requiring a new resolver interface.

- Cover qualified and relative instance/class methods, root targets, namespace-prefix lookup, and shadowing.
- Verify that both module-function spellings, with or without an owner, produce the same destination and one anchor.
- Cover reader/writer accessors, aliases, operator methods, Unicode owners, and Unicode method names.
- Verify labels, continuation lines, spacing, URL escaping, empty descriptions, and punctuation-only unresolved targets.
- Verify private, undocumented, missing, ambiguous, dynamic-receiver, and refinement cases retain text rather than linking to an unrelated declaration.
- Exercise multiple CLI input files, deterministic output, and cross-file ambiguity through the existing CLI.
- When explicit forms are implemented, verify BitClust/RD equivalence, bracket escaping, custom labels, literal code regions, and unchanged raw comments and source spans through the same seams.

## Out of Scope

Full RD rendering, YARD's `{target}` prose syntax, documentation copying via `(see target)`, runtime ancestry resolution, external reference databases, BitClust schemes beyond `m:` and `c:`, BitClust document/method fragment schemes, and the proposed `[label][m:target]` reference-style form are outside this decision. Unresolved-reference lint diagnostics are a separate decision; this ADR requires lossless fallback rather than a new warning policy.

## Further Notes

The explicit forms are a compatibility subset, not a claim of complete BitClust or YARD rendering compatibility. ADR 0001 keeps API documentation separate from contracts, ADR 0002 governs documentation markers and continuation, and ADR 0003 preserves the core/documentation package boundary.

Primary references:

- [BitClust Markdown specification, cross references](https://github.com/rurema/bitclust/blob/581943661862910ff2035df2974709aef816a1c0/doc/markdown-samples/MARKUP_SPEC.md#7-クロスリファレンスハイパーリンク).
- [BitClust Markdown reference implementation](https://github.com/rurema/bitclust/blob/581943661862910ff2035df2974709aef816a1c0/lib/bitclust/mdcompiler.rb#L849).
- [Rurema Markdown migration notes](https://blog.n-z.jp/blog/2026-07-23-rurema-to-markdown.html).
- [YARD reference syntax and automatic `@see` linking](https://github.com/lsegal/yard/blob/3d8670543d2408baea3a1360b2258fde2ee65917/docs/GettingStarted.md#L395-L409).
- [YARD `@see` tag definition](https://github.com/lsegal/yard/blob/3d8670543d2408baea3a1360b2258fde2ee65917/lib/yard/tags/library.rb#L503-L513).
- [YARD namespace lookup](https://github.com/lsegal/yard/blob/3d8670543d2408baea3a1360b2258fde2ee65917/lib/yard/registry_resolver.rb#L50-L100).
20 changes: 20 additions & 0 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,26 @@ For example, `@param path [String] — The file path.` carries an optional docum

Tags without a name or type write their description directly: `@return Normalized value.`, `@note Keep this in mind.`, and `@example Call the method.` Named tags such as `@param`, `@yieldparam`, `@raise`, and `@option` require the marker. `@option options :format — Output format.` documents an option. `@yieldreturn yielded value.` describes a yielded result. Supported tags include `@param`, `@return`, `@yieldparam`, `@yieldreturn`, `@option`, `@raise`, `@note`, `@see`, `@deprecated`, and `@example`.

### See also references

The complete syntax decision, including planned BitClust and RD compatibility forms, is recorded in [ADR 0004](adr/0004-documentation-reference-syntax.md).

`@see` accepts YARD-style references followed by an optional display label, without an em dash:

```ruby
# @see Foo#bar
# @see Foo.build Build a Foo
# @see #bar Related instance method
# @see .build Related class method
# @see Kernel.#puts Legacy module-function spelling
# @see Kernel?.puts Modern module-function spelling
# @see https://example.com/reference Reference guide
```

`#` selects an instance method and `.` selects a class method. `.#` and `?.` are equivalent module-function spellings and link to the public singleton copy. Relative references use the current class or module; qualified references search namespace prefixes as YARD does, and a leading `::` starts at the root. This lookup also applies to qualified class openings such as `class Outer::Caller`; it does not reproduce Ruby's lexical constant lookup. A nearer declared owner prevents fallback to an unrelated outer owner when its member is missing. The first whitespace-separated token is the target and the remaining text is the display label. HTTP, HTTPS, and mailto targets become external links.

`zard-doc` links references to uniquely identified public declarations with documentation in the generated output. The CLI resolves references across all input files. Unresolved, undocumented, private, ambiguous, and refinement references remain plain text. Lookup uses explicit declarations, without following inheritance or mixins or evaluating dynamic receivers. The document model retains the original description, raw comment, and source span.

Plain comment lines continue the preceding canonical documentation tag until another annotation or contract begins. No continuation marker is needed. Contract notes use ` -- ` after the contract payload and remain contract notes, not API prose. Familiar YARD-like tags without the em dash can be retained as raw text, but are not treated as documentation claims.

ZARD documentation requires UTF-8 source text. Ruby source encoding declarations are respected; non-UTF-8 source can still contain contracts, but documentation text in it receives a diagnostic.
Expand Down
30 changes: 30 additions & 0 deletions test/zard_doc_cli_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,36 @@ def test_render_reads_standard_input_from_a_dash
assert_empty stderr
end

def test_render_links_see_references_across_files
Dir.mktmpdir("zard-doc") do |directory|
File.write(File.join(directory, "a.rb"), "class Foo\n # Target.\n def bar = nil\nend\n")
File.write(File.join(directory, "b.rb"), "# @see Foo#bar Target\ndef call = nil\n")

status, stdout, stderr = run_cli("render", directory)

assert_equal 0, status
assert_includes stdout, "- [Target](#zard-466f6f23626172)"
assert_equal 1, stdout.scan('<a id="zard-466f6f23626172"></a>').length
assert_empty stderr
end
end

def test_render_keeps_ambiguous_see_references_as_text
Dir.mktmpdir("zard-doc") do |directory|
%w[a b].each do |name|
File.write(File.join(directory, "#{name}.rb"), "class Foo\n # Target.\n def bar = nil\nend\n")
end
File.write(File.join(directory, "c.rb"), "# @see Foo#bar Target\ndef call = nil\n")

status, stdout, stderr = run_cli("render", directory)

assert_equal 0, status
assert_includes stdout, "- Foo#bar Target"
refute_includes stdout, "<a id="
assert_empty stderr
end
end

private

def run_cli(*arguments, stdin: "")
Expand Down
Loading
Loading