Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add api and style section about documentation #8

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
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
Binary file added guidelines.pdf
Binary file not shown.
1 change: 1 addition & 0 deletions src/chapters/api.typ
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
#set heading(offset: 2)
#include "api/flexibility.typ"
#include "api/simplicity.typ"
#include "api/documentation.typ"
110 changes: 110 additions & 0 deletions src/chapters/api/documentation.typ
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
#import "/src/util.typ": *
#import mantys: *

= Documentation <sec:api:docs>
Documentation comments are important for consumers of the API as well as new contributors to your package or project to understand the usage and purpose of an item.
An item may be a state variable, a counter, a function, a reusable piece of regular content, or anything that can be bound using a `let` statement.
tingerrr marked this conversation as resolved.
Show resolved Hide resolved

#wbox[
For the exact syntax used for documentation comments, refer to @sec:style:docs.
]

== Visibility
For public functions the documentation should always include all public parameters, their usage and purpose, as well as possible constraints on their values.
Internal arguments may be omited, if they are not meant to be exposed to a user, but should still be documented for contributors in the source or a contribution document.
Only optional arguments may be internal, required arguments must always be documented and therefore public.

== Defaults
Default values should not be documented if they are simple expressions such as `"default"` or `5`.
If default values refer to more complex expressions or bindings form other modules which are not evident from the context of the documentation, then they should be documented in some way.

#do-dont[
```typst
// - a default is not documented when not necessary
// - usage of internal default binding default-b is documented and explained

/// Function doc
///
/// / a (): A doc.
/// / b (): B doc, defaults to ...
#let func(a: 5, b: internal.default-b) = { ... }
```
][
```typst
// - a default is unecessarily documented
// - user may not know what the default of b refers to

/// Function doc
///
/// / a (): A doc, defaults to 5.
/// / b (): B doc.
#let func(a: 5, b: internal.default-b) = { ... }
```
]

== Annotations
Implicit requirements such as contexts should be documented using property annotations (see @sec:style:docs).
Other annotations may be used to document additional invariants such as visibility or deprecation status.
Consider elaborating on all unconventional uses of property annotations in your documentation.

== Return Values
Functions should also document their return type, even if it is general, such as `any`.
Should a function return different types depending on it's inputs or the state of the document, it must be documented when this may happen or at least which types to expect and check for at the call site.

== States & Counters
Values like states and counters, if exposed, should document their invariants, such as the allowed types for states or the expected depth range of a counter for example.
The type of the value itself should likewise be annotated using the function return type syntax.

#do-dont[
```typst
// - well documented invariants
// - most doc parsers will identify named defaults, we omit them for brevity
// in this example

/// Does things.
///
/// This function is contextual if `arg` == `other`.
tingerrr marked this conversation as resolved.
Show resolved Hide resolved
///
/// / arg (int): Fancy arg, must be less than or equal to `other`.
/// / other (int): Other fancy arg, must be larger than `arg`.
/// -> content
#let func(arg, other: 1) = {
assert(arg <= other)

if arg == other {
context { ... }
} else {
...
}
}
```
][
```typst
// user don't know how these definitions must be shaped, optional fields on
// dictionaries especially can make it hard to get a full grasp of those implicit
// invariants if they can only inspect the state var in their doc to reverse
// engineer the definition shape

/// Contains the function definitions.
#let item = state("defs", (:))

// - invariants of `arg` are not documented and may result in poor UX
// - other is not documented
// - does not document contexuality

/// Does things.
///
/// / arg (any): Fancy arg.
#let func(arg, other: 1) = {
// the lack of typing information would cause a hard to understand error
// message for a novice user
assert(arg <= other)

if arg == other {
context { ... }
} else {
...
}
}
```
]
1 change: 1 addition & 0 deletions src/chapters/style.typ
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@
#include "style/sugar.typ"
#include "style/trailing.typ"
#include "style/linebreaks.typ"
#include "style/documentation.typ"
244 changes: 244 additions & 0 deletions src/chapters/style/documentation.typ
Original file line number Diff line number Diff line change
@@ -0,0 +1,244 @@
#import "/src/util.typ": *
#import mantys: *

= Documentation <sec:style:docs>
#wbox[
This section documents the syntax of documentation comments, see @sec:api:docs for the contents and purpose of documentation comments.
]

Documentation is placed on a group of lines comments using three forward slashes `///` and a ascii space (`U+0020`).
These are called doc comments.
Doc comments may not be interrupted by other non-trivial tokens, i.e. a block of documentation must be one continuous sequence of doc line comments without interspersing empty lines, markup, or regular comments.

#do-dont[
```typst
/// A doc comment
/// spankning multiple
/// consecutive lines
```
][
```typst
///no leading space

/// interspersed by
//
/// a regular comment

/// interspersed by

/// an empty line

/// interspersed by
Hello World
/// content

/**
* This is a regular comment, not a doc comment.
*/
#let item = { ... }

// This is a regular comment, not a doc comment.
#let item = { ... }
```
]

Doc comments are placed right above the declaration they are attached to except for module doc comments.

#do-dont[
```typst
/// Documentation for func
#let func() = { ... }

```
][
```typst
/// Stray doc comment, not the doc for func

#let func() = {
/// Stray doc comment, not the doc for func
...
}

/// Stray doc comment, not the doc for func
```
]

Module doc comments are used to document modules and must not have a declaration, instead they refer to the file they are placed in and may only be declared once and as the first non-comment item in a file.
There may be regular comments before doc comments to allow placing internal documentation or license information at the top of the file.
Inner doc comments are currently not permitted anywhere else.

#do-dont[
```typst
// optional leading comments

/// Module doc

/// Function or value doc
#let item = { ... }
```
][
```typst
/// Function or value doc
#let item = { ... }

/// Stray doc comment, not the module or func doc
```
]

Outer doc comments may be used on `let` bindings only.

#do-dont[
```typst
/// Function doc
#let func() = { ... }

/// Value doc
#let value = { ... }
```
][
```typst
/// Stray doc comment, markup may not be documented
Hello World

/// Stray doc comment, imports my not be documented
#import "module.typ"

/// Stray doc comment, scopes may not be documented
#[
...
]
```
]

Doc comments contain a description of the documented item itself, as well as an optional semantic trailer.
The content of descriptions should generally be simple Typst markup and should not contain any scripting, (i.e. no loops, conditionals or function calls, except for `#link("...")[...]`).
This allows the documentation to be turned into markdown or plain text for language servers to send to editors.

== Description
As mentioned before, the description should be simple, containing mostly markup and no scripting.

== Semantic Trailer
The semantic trailer is fully optional, starts with an empty doc comment line to separate it from the description, and may contain:
- multiple `term` items for parameter type hints and descriptions,
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we stick to the term items (so / instead of -)? I'd also welcome opinions from @Myriad-Dreamin @SillyFreak and @JamesxX on that :)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it makes sense imo. My only concern is: is there any chance that we'd need colons as part of the term? For type annotations (which are of course a temporary issue), I would say this could become necessary:

/// / entry (key: str, value: str): a single entry, given as a `dict`

this would have to become

/// / entry: (key: str, value: str) a single entry, given as a `dict`

which imo is not as pleasant to read in the code

Copy link

@Myriad-Dreamin Myriad-Dreamin Jul 10, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can have

/ entry (key\: str, value\: str): a single entry, given as a `dict`

image

But this should be not perfect anymore.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Imho only the type belongs there (so dictionary) and not the keys it has. The latter should be described in the parameter description. Also, this will probably not be supported with upcoming built-in type annotations (at least I haven't seen any suggestions regarding this) and so it won't work anyway.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps this is best solved by using comments per parameter (as suggested before and on the discord) and then simply using -> types for these parameters too?

/// ...
#let func(
  /// A single entry, given as a `dict` with the keys `key` and `value` of type `str`.
  /// -> dictionary
  entry,
) = { ... }

Or with a destructuring pattern (up for debate):

/// ...
#let func(
  /// A single entry.
  /// -> (key: str, value: str)
  entry,
) = { ... }

I'll touch on this in the TLDR below.

- a return type hint `-> type`,
- and multiple property annotations (`#property("private")` or `#property("deprecated")`).

Types in type lists or return type annotations may be separated by `|` to indicate that more than one type is accepted, the exact types allowed depend on the doc parser, but the built in types are generally supported.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One could add somewhere here in the text that these type annotations are a temporary solution and in the future they will be built-in.

Copy link
Member Author

@tingerrr tingerrr Jul 10, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That likely goes for most of the doc comment syntax, so I will note it for doc comments in general.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay. Although I think this is the mos important upcoming change and the rest will likely stay (roughly) the same


Parameter description and return types (if present) are placed tightly together, property annotations if present are separated using another empty doc comment line.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In my opinion this part is not necessary because it is a little confusing (the wording could have more than one interpretation of how it is meant)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you mean the section about builtin types?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, the line 132 ("Parameter description and return types (if present) are placed tightly"). I explicitly marked this line in the review but the preview shows a little more ... ^^

By the way, it should be built-in on line 130


Parameter documentation is created by writing a term item containing the parameter name, a type list in parentheses and a description of the parameter.
If the parameter is an argument sink its name must likewise contain the spread operator `..`.
Each parameter can only be documented once, but doesn't have to.
Undocumented parameters are considered private by convention and may only be optional parameters.

#do-dont[
```typst
/// Function description
///
/// / b (any): b descpription
/// / a (int | float): a description
#let func(a, b, c) = { ... }

/// Function description
///
/// / ..args (int): args description
#let func(..args) = { ... }
```
][
```typst
/// Function description
///
/// / c (any): c doesn't exist
/// / a (int | float): a description
#let func(a, b) = { ... }

/// Missing empty line between function description and trailer
/// / b (any): b descpription
/// / a (int | float): a description
#let func(a, b) = { ... }


/// Function description
///
/// / b (any): b descpription
/// / a: missing types
#let func(a, b) = { ... }

/// Function description
///
/// / args (int): missing spread operator for args
#let func(..args) = { ... }
```
]

The return type can only be annotated once, on a single line after all parameters, if any exist.
A variable's documentation can also use the return type annotation syntax to annotate its own type.
Items which bind a `function.with` expression should be treated as regular function definitions, i.e. their return type is the return type of the function when called, not `function` itself as would be the return value of `function.with`.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

function.with() ?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and comma after i.e.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

function.with() ?

The example below explains this, shorthand declarations with partially bound parameters.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I meant adding the parentheses 😂

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, yes sure.


#do-dont[
```typst
/// Function doc
///
/// / arg (types): Description for arg
/// -> types
#let func(arg) = { ... }

/// Function doc
///
/// -> types
#let other = func.with(default)

/// Value doc
///
/// -> type
#let value = { ... }
```
][
```typst
/// Missing empty line between description trailer
///
/// -> type
/// / arg (types): arg descrption after return type
#let func(arg) = { ... }
```
]

Property annotations can be used to document package-specific or otherwise important information like deprecation status, visibility or contextuality.
Such annotations may only be used after the return type (if one exists) and an empty doc comment line.

#do-dont[
```typst
/// Function doc
///
/// / arg (types): Description for arg
/// -> types
///
/// #property("deprecated")

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm wondering whether we can simply have predefined properties, so they could be simpler:

// #import tidy: *
#deprecated
#contextual

Since we have module, we can also use the properties with "namespace"s to disambiguate:

#tidy.deprecated

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would argue this is an implementation detail, so I'd leave it up to the doc parsers and such to come up with a convention.

But we could define some defaults too, to avoid an ecosystem split from people choosing their own conventions.

/// #property("contextual")
#let func(arg) = { ... }

/// Value doc
///
/// #property("contextual")
#let value = { ... }
```
][
```typst
/// Missing empty line between return type and properties
///
/// / arg (types): Description for arg
/// -> types
/// #property("deprecated")
/// #property("contextual")
#let func(arg) = { ... }

/// Return type after properties and missing empty separation lines
///
/// / arg (types): Description for arg
/// #property("deprecated")
/// #property("contextual")
/// -> types
#let func(arg) = { ... }
```
]