Documentation
¶
Overview ¶
Package markdown is shithub's canonical markdown rendering pipeline.
One entry point: `Render(ctx, source, opts) (html, refs, mentions)`. Every comment, issue/PR body, README, and any future surface that takes user-authored markdown flows through here. No other code in the tree imports goldmark or bluemonday directly; a lint guard at `scripts/lint-markdown-boundary.sh` enforces that boundary.
What's supported (CommonMark + a curated GFM set):
- Headings, paragraphs, lists, blockquotes, code blocks (fenced + indented).
- GFM tables, strikethrough, autolinks, task lists.
- `@username` mentions — resolved via Options.Resolvers.User.
- `#N` and `owner/repo#N` references — resolved via Options.Resolvers.Issue, gated by viewer visibility.
- Emoji shortcodes (`:smile:`) — curated set in `emoji/`.
- `<details>` / `<summary>`, `<kbd>`, `<sup>`, `<sub>`, task-list checkboxes (output by Goldmark with `disabled`).
- Code-block class allowlist: `language-*` (Chroma uses these).
What's deliberately not supported:
- Raw HTML beyond the strict allowlist (we do NOT match GitHub's loose HTML acceptance — we err safer).
- `data:` URIs (any flavor; documented in docs/markdown.md).
- `javascript:` / `vbscript:` URLs (rejected by sanitizer).
- GFM Footnotes (deferred).
- Math (KaTeX), Mermaid, embedded media (post-MVP).
Pipeline-version contract:
- `Version` (in version.go) stamps every render.
- Callers store the version alongside cached HTML.
- On read, callers compare the stored version to `Version`; if they differ, the cache is stale and the caller re-renders. We never run a one-shot "re-render every comment" job — lazy.
Performance budget:
- 50 KiB body, full extensions: <30 ms p99 on MVP hardware.
- Inputs above MaxRenderInputBytes are rejected up-front; callers enforce the matching cap at the API layer.
Index ¶
- Constants
- Variables
- func Render(ctx context.Context, src []byte, opts Options) (rendered []byte, refs []Ref, mentions []Mention, err error)
- func RenderDocumentHTML(src []byte) (string, error)
- func RenderHTML(src []byte) (string, error)
- type Mention
- type Options
- type Ref
- type RepoContext
- type Resolvers
Constants ¶
const MaxRenderInputBytes = 1 << 20 // 1 MiB
MaxRenderInputBytes caps the input body. Comments / bodies are rejected at the API layer at 64 KiB or 256 KiB depending on surface; this is the renderer's defensive fallback.
const Version int32 = 1
Version is the canonical pipeline-version stamp written to `body_html_cached`-style columns whenever a comment / issue body / PR body is rendered. Bumping this constant invalidates cached HTML lazily on read: callers compare the stored `md_pipeline_version` to `Version` and re-render when they don't match.
Bump rules:
- Sanitizer policy change (allow/disallow tag, attribute, scheme).
- New AST extension or rendering output change.
- Goldmark / bluemonday major-version upgrade with output drift.
Don't bump for:
- Bug fixes that don't change rendered HTML for any input.
- Performance-only changes.
Variables ¶
var ErrInputTooLarge = errors.New("markdown: source exceeds MaxRenderInputBytes")
ErrInputTooLarge is returned when source exceeds MaxRenderInputBytes. Callers should reject the input at the API layer before reaching Render; this is the renderer's defensive fallback.
Functions ¶
func Render ¶
func Render(ctx context.Context, src []byte, opts Options) (rendered []byte, refs []Ref, mentions []Mention, err error)
Render is the canonical markdown entry point. The output bytes have already been Goldmark-rendered AND bluemonday-sanitized; callers can wrap them as `template.HTML` and inject into a template directly.
`refs` and `mentions` carry the resolved cross-reference/mention state for downstream consumers (S29 notification fan-out, S21 issue_references index). The order matches first-occurrence in source.
A nil ctx is fine; resolvers receive context.Background() in that case.
func RenderDocumentHTML ¶
RenderDocumentHTML renders README-like markdown where source line wrapping should remain semantic markdown, not comment-style hard breaks.
func RenderHTML ¶
RenderHTML is the back-compat shim for callers that don't need resolved refs/mentions. SoftBreakAsBR defaults to true (matches the comment-style legacy behavior of the S17 helper this replaces).
The signature matches `internal/repos/markdown.RenderHTML` so existing callers can swap the import path without code changes. New callers should prefer Render directly.
Types ¶
type Mention ¶
Mention is one resolved @username mention. S29's fan-out consumes the deduplicated list returned by Render.
type Options ¶
type Options struct {
// Repo is the binding for same-repo `#N` resolution. nil means
// "no repo context"; `#N` renders as plain text.
Repo *RepoContext
// ViewerUserID gates cross-repo `#N` and `@user` resolution.
// 0 = anonymous viewer.
ViewerUserID int64
// SoftBreakAsBR controls newline-as-<br> rendering:
// true — comment / issue / PR body shape (matches GitHub UI)
// false — README / structured-doc shape (preserve markdown semantics)
SoftBreakAsBR bool
// LinkTargetBlank, when true, sets target="_blank" rel="noopener
// noreferrer" on autolinks + reference links. Default off; READMEs
// keep links in-page so the user doesn't lose context.
LinkTargetBlank bool
// Resolvers wires reference resolution. nil resolvers in the
// struct mean "render that flavor as plain text".
Resolvers Resolvers
}
Options tunes a single Render call. Zero-value Options is valid and yields a safe, generic render with no reference resolution.
type Ref ¶
type Ref struct {
// Kind is "issue" or "commit" for now.
Kind string
// Same-repo refs leave Owner/Repo empty.
Owner string
Repo string
// Number is set for issue refs; FullSHA for commit refs.
Number int64
FullSHA string
// Href is the resolved URL the renderer wrote into the document.
Href string
}
Ref is one resolved reference produced during rendering. The caller uses these for downstream notification fan-out (S29) and for the issue_references index (S21).
type RepoContext ¶
RepoContext is the optional repo binding for `#N` reference resolution. When nil, `#N` renders as plain text (matches "repo-context-less rendering" in the spec).
type Resolvers ¶
type Resolvers struct {
// User: @username → "/username" if the user exists, isn't
// suspended, and (post-S30) is org-team-visible to the viewer.
User func(ctx context.Context, username string) (href string, ok bool)
// Issue: (repoOwner, repoName, number) → "/owner/repo/issues/N".
// Returns ok=false when the repo isn't visible to viewer or the
// number wasn't allocated yet.
Issue func(ctx context.Context, owner, name string, number int64, viewerUserID int64) (href string, ok bool)
// Commit: short-or-full SHA inside the current repo context →
// "/owner/repo/commit/<full_sha>". Only invoked when
// Opts.Repo != nil.
Commit func(ctx context.Context, repoOwner, repoName, shaPrefix string) (href, fullSHA string, ok bool)
// Team: @org/team → "/org/teams/team". Visibility-aware: a
// secret team the viewer can't see should return ok=false so
// the renderer falls back to plain text. (S31)
Team func(ctx context.Context, orgSlug, teamSlug string, viewerUserID int64) (href string, ok bool)
}
Resolvers wire the package against the rest of the runtime without importing usersdb / issuesdb directly (avoids a cycle with packages that themselves render markdown). Each resolver is optional; a nil resolver means "render as plain text" for that flavor of reference.
All resolvers MUST be visibility-aware: they receive the viewer (Options.ViewerUserID) and return ok=false when the resource isn't visible. Returning a link to a hidden resource leaks existence.
Directories
¶
| Path | Synopsis |
|---|---|
|
Package extensions hosts the AST transformer that adds shithub- specific inline patterns (`@user`, `#N`, `owner/repo#N`, commit SHAs, emoji shortcodes) to Goldmark's parsed text without ever touching the contents of code blocks or inline code.
|
Package extensions hosts the AST transformer that adds shithub- specific inline patterns (`@user`, `#N`, `owner/repo#N`, commit SHAs, emoji shortcodes) to Goldmark's parsed text without ever touching the contents of code blocks or inline code. |