Documentation
¶
Overview ¶
Package repo discovers, parses and relates the documents of a spec repository. Nothing here reads stdin, prompts, or exits.
Index ¶
- Constants
- Variables
- func Anchor(text string) string
- func AnchorsIn(source []byte) []string
- func Contains(root, full string) error
- func EscapeMarkdown(text string) string
- func HeadingSurvives(text string) bool
- func Identifier(t Type, number int) string
- func ImplementedIn(includedIn []string) []string
- func IsHeading(line string) bool
- func LineEnding(source []byte) string
- func LinkDestination(path string) string
- func Prose(body string, bodyLine int) iter.Seq[ProseLine]
- func Renumber(r *Repo, from, to string, frozen func(*Document) bool) ([]string, error)
- func RequiredKeys(t Type) []string
- func RequiredSections(t Type) []string
- func Schema(t Type) []string
- func SetField(source []byte, key string, line int, value string) ([]byte, error)
- func Slug(title string) string
- func StartOfDay(t time.Time) time.Time
- func UnclosedIn(source []byte) bool
- func WriteFile(full string, source []byte, name string) error
- type Document
- func (d *Document) ClosingSection() (string, bool)
- func (d *Document) Contents() []Entry
- func (d *Document) H1() (Heading, bool)
- func (d *Document) Headings() []Heading
- func (d *Document) Links() []Link
- func (d *Document) Prose() iter.Seq[ProseLine]
- func (d *Document) Repo() *Repo
- func (d *Document) RequiredSections() []string
- func (d *Document) SectionTitles() []string
- func (d *Document) Sections(title string) []Section
- func (d *Document) Unclosed() bool
- func (d *Document) WikiLinks() []WikiLink
- type Entry
- type FrontMatter
- type GlossaryEntry
- type Heading
- type Link
- type LinkType
- type Problem
- type ProseLine
- type Repo
- func (r *Repo) ByID(id string) *Document
- func (r *Repo) ByPage(page string) *Document
- func (r *Repo) ByPath(path string) *Document
- func (r *Repo) Config() *config.Config
- func (r *Repo) Documents() []*Document
- func (r *Repo) File(docPath string) string
- func (r *Repo) Glossary() (entries []GlossaryEntry, ok bool)
- func (r *Repo) Next(t Type, title string) (id, path string, err error)
- func (r *Repo) Term(name string) (GlossaryEntry, bool)
- func (r *Repo) WriteFile(docPath string, source []byte) error
- type Section
- type Status
- type Type
- type UnknownKey
- type WikiLink
Constants ¶
const DateLayout = "2006-01-02"
DateLayout is the ISO 8601 date form every date field uses.
const GlossaryPage = "glossary"
GlossaryPage is the spec page name the glossary always has.
const RejectionRationale = "Rejection rationale"
RejectionRationale is the section a rejected document gains, and which no other document may carry.
const Sources = "Sources"
Sources names what a document was checked or reconstructed against. Required on every ref, and on any backfilled document.
Variables ¶
var ( // The same two rules as linkPattern and wikiLinkPattern above, which carry // a capture group the maskers do not need. A group costs about 1% on these // inputs, so one declaration each is the better trade: they were three // separate spellings of two rules and could drift. MarkdownLinkPattern = linkPattern ReferenceLinkPattern = regexp.MustCompile(`!?\[[^\]]*\]\[[^\]]*\]`) WikiLinkPattern = wikiLinkPattern // WikiLinkSubmatchPattern is the same compiled rule under a second name, for // a caller that reads the name out rather than only blanking the span. One // declaration, as above: the two names mark the two uses. WikiLinkSubmatchPattern = wikiLinkPattern )
Patterns for spans a suggestion must never be offered inside: an inline link, a reference link, or an unexpanded wiki link. Wrapping text inside any of them produces markup that means something else entirely, and in the wiki case it hides the very error that was being reported.
var BlankLinePattern = regexp.MustCompile(`\r?\n[ \t]*\r?\n`)
BlankLinePattern separates paragraphs. It matches CRLF as well as LF, because a file saved on Windows would otherwise read as one unbroken paragraph and L15 would pass anything. Exported for the same reason as FormerlyPattern: internal/glossary refuses a definition that contains one.
var FormerlyPattern = regexp.MustCompile(`^Formerly \*(.+)\*\.$`)
FormerlyPattern matches the line a rename leaves behind. It is not a paragraph, and several may accumulate as a term is renamed repeatedly.
Exported because internal/glossary asks the same question when it decides where to put a new note. A second spelling of the rule there could drift from this one with nothing to notice, which is why the link patterns are exported too.
var Statuses = []Status{StatusDraft, StatusProposed, StatusAccepted, StatusRejected, StatusWithdrawn}
Statuses is every valid status.
Types is every document type, in directory order.
Functions ¶
func Anchor ¶
Anchor is the fragment GitHub would generate for a heading standing on its own. Headings inside a document take their anchors from a walk that numbers repeats, so this is only for naming the anchor a heading would have had when the heading is not there to ask.
func AnchorsIn ¶
AnchorsIn returns the heading anchors of arbitrary markdown, for checking a link into a file that is not itself a document.
func Contains ¶
Contains reports whether full sits inside root once every directory component has been resolved.
config.ValidateRoot already checks the root itself, and that is not enough: a root can be perfectly ordinary while a document directory inside it points elsewhere, and every write into that directory then lands outside the repository while the tool reports a path inside it. A parent that does not exist yet is fine, because it will be created inside root.
func EscapeMarkdown ¶
EscapeMarkdown makes arbitrary text safe to place inside generated markdown: a table cell, or a link label. A title is free text, and a document called "Pwned](https://evil.example) x" would otherwise become a live hyperlink in another document's body and in the index, with lint reporting nothing.
The backslash goes first, or escaping the others would double it.
func HeadingSurvives ¶
HeadingSurvives reports whether text written as a heading reads back as itself. A trailing hash is a closing sequence and trailing whitespace is discarded, so a title carrying either produces an H1 that no longer matches the front matter, and a glossary term that can never be found by the name that was typed.
func Identifier ¶
Identifier renders the identifier for a type and number, as RFC-0001.
func ImplementedIn ¶
ImplementedIn is the spec pages whose inclusion of a document means the document has been built, which is every page but the glossary.
The full IncludedIn list is kept as written, because "which spec pages reference this" is a separate and factual question.
func IsHeading ¶
IsHeading reports whether a line is an ATX heading. A heading's raw text is a section's identity throughout this package and in lint, so a rewriter must leave one alone.
func LineEnding ¶
LineEnding is the ending a file uses, so that an edit does not leave it with a mixture. The first line decides it. Three packages were deciding this separately, and one of them forgot.
func LinkDestination ¶
LinkDestination renders a path as a markdown link destination, percent- encoding every character that would otherwise end the destination, the link, or the table cell around it.
One function, because the index and the link rewriter each rendered destinations their own way and only one of them escaped anything. Encoding rather than escaping, because the two interact: the angle-bracket form this replaces turned a path's ">" into "\>", and a backslash already in the path then paired with that backslash and let the ">" close the destination early. An ordinary path contains none of these and comes through unchanged, and Document.Links decodes on the way back in, so the two are inverses.
func Prose ¶
Prose iterates the prose lines of a body: those not inside a fenced code block or an HTML comment. Leaving the loop stops the walk.
func Renumber ¶
Renumber changes a numbered document's identifier, rewriting the filename, the front matter id, the heading, and every reference to the old identifier in every other document.
The number lives in three places inside its own document and in an unbounded number of places outside it, so doing this by hand reliably trades one lint error for another. Two concurrent pull requests taking the same number is the case that makes it routine.
frozen reports whether a document may no longer be written to. Renumbering refuses when the document itself is frozen, and when any document that would have to change is, because a reference inside a frozen document could never be corrected and the rename would leave it pointing at a file that is gone. The refusal is complete: nothing is written unless everything can be.
func RequiredKeys ¶
RequiredKeys lists the keys that must be present, whatever their value.
The schema may only ever gain optional keys. A new required key would put every frozen document in every existing repository in violation of L01, and L11 forbids the edit that would add it.
func RequiredSections ¶
RequiredSections lists the H2 sections a type must carry, in the order they must appear. Other sections are permitted between or after them.
func Schema ¶
Schema lists the keys a type's front matter may carry. A key outside this list is an unknown field.
func SetField ¶
SetField replaces the value of one front matter key, on the line it was written, and leaves everything else exactly as it was: the order of the other keys, their spacing, and any comments. Re-serialising the block through a YAML encoder would be shorter and would quietly discard all three.
func Slug ¶
Slug derives the human-readable half of a filename from a title: lowercased, runs of anything else collapsed to one hyphen, and the ends trimmed. The number is the identity; this is only for reading.
func StartOfDay ¶
StartOfDay is an instant's calendar date at midnight UTC, which is how a date written in front matter is held once parsed.
It lives beside DateLayout because every comparison between a written date and "now" needs it. Comparing a parsed date against a wall-clock instant makes the answer depend on the hour the command ran and the zone it ran in: east of UTC it read today as the future, and west of it read tomorrow as the past. That was a defect twice, in two packages, before this was shared.
func UnclosedIn ¶
UnclosedIn is the same question about a whole file that is not a Document yet, which is what a writer has in hand just before it returns.
func WriteFile ¶
WriteFile writes a file, refusing to write through a symbolic link. name is what the refusal calls the path, which for a document is its repository path.
Every writer in the program must come through here. os.WriteFile follows a symlink and truncates whatever is on the other end, so a symlinked INDEX.md committed to a repository, which git stores and clone restores, redirected `archdoc index` onto any file the user could write, reported only "INDEX.md", and exited 0.
Types ¶
type Document ¶
type Document struct {
Type Type
// Path is relative to root, always slash-separated.
Path string
// ID is the identifier derived from the type and the filename number, such
// as RFC-0001. Empty for spec pages, which are not numbered. This is the
// authoritative identity; lint compares the front matter id against it.
ID string
// Number is the filename number for numbered types, 0 otherwise.
Number int
// Page is a spec page's path below spec/ without the extension, such as
// http/routing. Empty for numbered types.
Page string
// FrontMatter is the parsed YAML block.
FrontMatter FrontMatter
// Body is everything after the closing delimiter.
Body string
// BodyLine is the 1-based line the body starts on, so that positions
// found in the body can be reported against the file.
BodyLine int
// Source is the document exactly as it is on disk, never normalised on the
// way in, because every writer splices into it by offset.
Source []byte
// Problems are the faults found while reading the document. Lint decides
// what they mean; repo only records them.
Problems []Problem
// UpdatedBy, ObsoletedBy and DependedOnBy hold the identifiers of the
// documents naming this one in the matching forward list. IncludedIn holds
// the names of the spec pages listing this document in includes. All are
// sorted and derived, never written to the document itself.
UpdatedBy []string
ObsoletedBy []string
DependedOnBy []string
IncludedIn []string
// EffectivelyObsolete is true when an accepted document obsoletes this one.
// A claim by a draft, proposed, rejected or withdrawn document has no
// effect.
EffectivelyObsolete bool
// Implemented is true for an accepted RFC or ADR that some spec page
// includes.
Implemented bool
// Stale is true for a spec page including an effectively obsolete document.
Stale bool
// contains filtered or unexported fields
}
Document is one file in one of the four document directories.
func (*Document) ClosingSection ¶
ClosingSection is the section that must be a document's last H2, and whether it has one at all. Most documents do not: PROCESS.md states a closing section only for a ref and for a rejected document, and extra sections may otherwise sit after the required ones.
It is the last entry of RequiredSections when that entry is a closing kind, which is what keeps this rule and the relative-order rule from contradicting each other. They did: Sources was required last and the rationale required after Sources, so a backfilled rejected document could satisfy neither, and `archdoc new --backfill --status=rejected` wrote one that could never lint clean and that L11 then forbade anyone repairing. PROCESS.md's Document structure section calls the rationale final and its Backfilling section asks only that a backfilled document carry Sources, so the rationale closes the document when both are required.
func (*Document) Contents ¶
Contents is the document as a flat list of its headings, in document order.
func (*Document) Links ¶
Links returns every markdown link in the body, ignoring code. A link reference definition counts: its destination is a target like any other, and nothing was checking it.
func (*Document) RequiredSections ¶
RequiredSections is RequiredSections for the type, plus Sources when the document is backfilled. Both L08, which checks a section is present, and L14, which checks it is not empty, read this one list, so they cannot disagree about what a document must carry.
func (*Document) SectionTitles ¶
SectionTitles lists the document's H2 headings in order.
func (*Document) Unclosed ¶
Unclosed reports whether the body ends inside an HTML comment or a fenced code block.
Everything from such a marker to the end of the file is masked, which is correct, and it means the document's headings and sections stop there: the parse is a prefix of what the author wrote. A writer that computes a line range from that parse and splices on it cuts across the gap, which is how `term remove` deleted every entry below an entry holding a stray `<!--`, silently, on a page that lint passed. Writers ask this before they splice.
type Entry ¶
type Entry struct {
Heading Heading
// Text is the body as written, from the line after the heading to the line
// before the next one, so Heading.Line plus one is its first line.
Text string
}
Entry is one heading and the lines beneath it, up to the next heading of any level, whatever that heading's level is.
A Section is a different unit and stays one: it is an H2 and everything under it, subsections included, which is what the required-section rules mean by the word. An entry's body holds no other entry's prose, so walking the entries reads every line of a body exactly once.
type FrontMatter ¶
type FrontMatter struct {
ID string
Title string
Status Status
Created time.Time
Decided time.Time
Depends []string
Updates []string
Obsoletes []string
Includes []string
Verified time.Time
// Backfilled is the date the document was written to record a decision
// taken earlier. Optional, and the only optional key: created and decided
// carry the historical dates, this one says when they were written down.
Backfilled time.Time
// contains filtered or unexported fields
}
FrontMatter holds every field any document type may carry. Fields that do not apply to a type stay zero; lint decides which are permitted where.
func (FrontMatter) Has ¶
func (f FrontMatter) Has(key string) bool
Has reports whether a key was written in the block, whatever its value.
func (FrontMatter) LineOf ¶
func (f FrontMatter) LineOf(key string) int
LineOf is the line a key was written on, or 0 if it was not.
func (FrontMatter) Unknown ¶
func (f FrontMatter) Unknown() []UnknownKey
Unknown lists keys written in the block that belong to no schema.
type GlossaryEntry ¶
type GlossaryEntry struct {
Term string
Anchor string
// Line is 1-based within the file, at the term's heading.
Line int
// End is the 1-based line just past the entry: the next heading of level
// two or less, or 0 when the entry runs to the end of the file.
//
// It is recorded here rather than derived from the next entry's Line,
// because those two answers differ whenever an ordinary H1 section sits
// among the entries, and the difference deleted that section.
End int
// Paragraphs holds the entry's prose. A well-formed entry has exactly one;
// L15 reports the rest.
Paragraphs []string
// Formerly holds the previous names recorded by `Formerly *Old Term*.`
// lines, which are not paragraphs.
Formerly []string
}
GlossaryEntry is one term in spec/glossary.md.
func GlossaryIn ¶
func GlossaryIn(source []byte) ([]GlossaryEntry, bool)
GlossaryIn parses glossary entries out of arbitrary source, so that a rewritten page can be read back without being written to disk first.
type Heading ¶
type Heading struct {
Level int
Text string
// Line is 1-based within the file.
Line int
// Anchor is the fragment GitHub would generate for this heading.
Anchor string
// contains filtered or unexported fields
}
Heading is one markdown heading in a document body.
type Link ¶
type Link struct {
Text string
Target string
Type LinkType
// Line is 1-based within the file.
Line int
}
Link is a relative markdown link found in a body.
type LinkType ¶
type LinkType string
LinkType is the form a link was written in. An image resolves like any other link and renders as something else entirely, so a consumer rewriting destinations has to tell them apart.
type Problem ¶
type Problem struct {
// Line is 1-based within the document, or 0 when it is not known.
Line int
Message string
}
Problem is something wrong with a document, found while reading it. Lint turns problems into findings; repo does not decide severity or rule identity.
type ProseLine ¶
type ProseLine struct {
// Raw is the line as written. Edits are applied to this one.
Raw string
// Masked is Raw with the contents of inline code spans and HTML comments
// blanked, keeping their length so that a position found here is the same
// column in Raw. Matches are looked for in this one.
Masked string
// Number is the line's 1-based number in the file.
Number int
}
ProseLine is one line of a body that is neither inside a fenced code block nor inside an HTML comment.
The two strings are a pair, and which is which matters: a caller decides from Masked and applies the edit that follows to Raw, at the same offsets. They are named fields rather than adjacent parameters of the same type because confusing the two is the single root cause of three defects this package has shipped.
type Repo ¶
type Repo struct {
// contains filtered or unexported fields
}
Repo is a spec repository: its configuration and every document under root.
func (*Repo) ByID ¶
ByID returns the document with this identifier, or nil. The identifier is the one derived from the type and filename, not whatever the front matter claims.
func (*Repo) File ¶
File is the absolute location of a document path, which is stored slash-separated and relative to root.
func (*Repo) Glossary ¶
func (r *Repo) Glossary() (entries []GlossaryEntry, ok bool)
Glossary parses spec/glossary.md. ok is false when the page does not exist, which is permitted: a repository need not have one until a term is added.
func (*Repo) Next ¶
Next returns the identifier and path a new document of this type would take. The number is one past the highest in the directory, so a gap left by a document that was never written is not reused.
type Section ¶
type Section struct {
Heading Heading
// Text is the section's body as written, which is what a rewrite needs.
Text string
// contains filtered or unexported fields
}
Section is a heading and everything under it, up to the next heading of equal or higher level.
type Status ¶
type Status string
Status is the lifecycle status of an RFC or ADR. Spec pages and refs have none.
type Type ¶
type Type string
Type is one of the four kinds of document, and also the directory each kind lives in.
func ParseIdentifier ¶
ParseIdentifier reads an identifier back into the type and number it names.
func ParseType ¶
ParseType turns user input into a Type. Type is a string type with no constructor, so Type("nonsense") is otherwise a perfectly valid value and every caller would need its own list of the real ones.
func TypeOf ¶
TypeOf classifies a path relative to root. ok is false when the path is not a document: the wrong extension, outside the four directories, or nested in a type that does not permit subdirectories.
func (Type) HasLifecycle ¶
HasLifecycle reports whether documents of this type move through statuses. A spec page reflects what has already been decided and a ref records something outside the project, so neither has a status at all.
func (Type) Normative ¶
Normative reports whether a document of this type may be named in a depends or includes list. Those name designs and decisions; a ref is informational and is cited from the body instead.
It answers the same as HasLifecycle today and is a separate question: one is about whether a document can change state, the other about whether anything may rest on it. A fifth type would very likely separate them.
type UnknownKey ¶
UnknownKey is a front matter key belonging to no schema, and where it was written.