markdown

package
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 10, 2026 License: AGPL-3.0 Imports: 11 Imported by: 0

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

View Source
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.

View Source
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

View Source
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

func RenderDocumentHTML(src []byte) (string, error)

RenderDocumentHTML renders README-like markdown where source line wrapping should remain semantic markdown, not comment-style hard breaks.

func RenderHTML

func RenderHTML(src []byte) (string, error)

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

type Mention struct {
	Username string
	Href     string
}

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

type RepoContext struct {
	OwnerUsername string
	RepoName      string
	RepoID        int64
}

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL