docs API

docs

package

API reference for the docs package.

S
struct

plugin

cmd/rfw/plugins/docs/docs.go:19-21
type plugin struct

Methods

Name
Method

Returns

string
func (*plugin) Name() string
{ return "docs" }
Priority
Method

Returns

int
func (*plugin) Priority() int
{ return 0 }
Build
Method

Parameters

Returns

err error
func (*plugin) Build(raw json.RawMessage) (err error)
{
	cfg := struct {
		Dir  string `json:"dir"`
		Dest string `json:"dest"`
	}{
		Dir:  "articles",
		Dest: filepath.Join("build", "static"),
	}
	if len(raw) > 0 {
		if err := json.Unmarshal(raw, &cfg); err != nil {
			return fmt.Errorf("decode docs plugin config: %w", err)
		}
	}
	sourcePath, err := docsProjectDirectory(cfg.Dir)
	if err != nil {
		return fmt.Errorf("invalid docs source: %w", err)
	}
	destinationPath, err := docsProjectDirectory(cfg.Dest)
	if err != nil {
		return fmt.Errorf("invalid docs destination: %w", err)
	}
	p.src = sourcePath
	base := filepath.Base(sourcePath)
	destRoot := filepath.Join(destinationPath, base)

	projectRoot, err := os.OpenRoot(".")
	if err != nil {
		return err
	}
	defer func() {
		if closeErr := projectRoot.Close(); err == nil {
			err = closeErr
		}
	}()
	if err := projectRoot.MkdirAll(destRoot, 0o755); err != nil {
		return err
	}
	sourceRoot, err := projectRoot.OpenRoot(sourcePath)
	if err != nil {
		return err
	}
	defer func() {
		if closeErr := sourceRoot.Close(); err == nil {
			err = closeErr
		}
	}()
	targetRoot, err := projectRoot.OpenRoot(destRoot)
	if err != nil {
		return err
	}
	defer func() {
		if closeErr := targetRoot.Close(); err == nil {
			err = closeErr
		}
	}()
	err = fs.WalkDir(sourceRoot.FS(), ".", func(path string, entry fs.DirEntry, walkErr error) error {
		if walkErr != nil {
			return walkErr
		}
		if entry.IsDir() {
			if path == "." {
				return nil
			}
			if err := targetRoot.MkdirAll(path, 0o755); err != nil {
				return err
			}
			return nil
		}
		in, err := sourceRoot.Open(path)
		if err != nil {
			return err
		}
		if err := targetRoot.Remove(path); err != nil && !errors.Is(err, os.ErrNotExist) {
			return errors.Join(err, in.Close())
		}
		out, err := targetRoot.OpenFile(path, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0o644)
		if err != nil {
			return errors.Join(err, in.Close())
		}
		_, copyErr := io.Copy(out, in)
		closeErr := errors.Join(out.Close(), in.Close())
		return errors.Join(copyErr, closeErr)
	})
	return err
}
ShouldRebuild
Method

Parameters

path string

Returns

bool
func (*plugin) ShouldRebuild(path string) bool
{
	relative, err := filepath.Rel(p.src, path)
	return err == nil && relative != ".." && !strings.HasPrefix(relative, ".."+string(filepath.Separator))
}

Fields

Name Type Description
src string
F
function

init

cmd/rfw/plugins/docs/docs.go:23-23
func init()

{ plugins.Register(&plugin{}) }
F
function

docsProjectDirectory

Parameters

path
string

Returns

string
error
cmd/rfw/plugins/docs/docs.go:120-129
func docsProjectDirectory(path string) (string, error)

{
	if path == "" || filepath.IsAbs(path) {
		return "", fmt.Errorf("path %q must be project-relative", path)
	}
	cleaned := filepath.Clean(path)
	if cleaned == "." || cleaned == ".." || strings.HasPrefix(cleaned, ".."+string(filepath.Separator)) {
		return "", fmt.Errorf("path %q escapes the project", path)
	}
	return cleaned, nil
}
F
function

TestBuildAndShouldRebuild

TestBuildAndShouldRebuild ensures the docs plugin copies files from the
source directory to the destination and correctly reports rebuild needs.

Parameters

cmd/rfw/plugins/docs/docs_test.go:14-52
func TestBuildAndShouldRebuild(t *testing.T)

{
	p := &plugin{}

	tmp := t.TempDir()
	t.Chdir(tmp)
	src := "articles"
	dest := "out"
	if err := os.MkdirAll(filepath.Join(src, "a"), 0o750); err != nil {
		t.Fatalf("mkdir: %v", err)
	}
	srcFile := filepath.Join(src, "a", "doc.txt")
	if err := os.WriteFile(srcFile, []byte("hello"), 0o600); err != nil {
		t.Fatalf("write src: %v", err)
	}

	cfg := struct {
		Dir  string `json:"dir"`
		Dest string `json:"dest"`
	}{Dir: src, Dest: dest}
	raw, err := json.Marshal(cfg)
	if err != nil {
		t.Fatal(err)
	}
	if err := p.Build(raw); err != nil {
		t.Fatalf("Build: %v", err)
	}

	// File should be copied under dest/<basename>/a/doc.txt
	if data, err := os.ReadFile("out/articles/a/doc.txt"); err != nil || string(data) != "hello" {
		t.Fatalf("expected copied file, got %v %q", err, data)
	}

	if !p.ShouldRebuild(srcFile) {
		t.Fatalf("expected ShouldRebuild true for %s", srcFile)
	}
	if p.ShouldRebuild(filepath.Join(tmp, "other.txt")) {
		t.Fatalf("unexpected rebuild for unrelated file")
	}
}
F
function

TestBuildRejectsPathsOutsideProject

Parameters

cmd/rfw/plugins/docs/docs_test.go:54-66
func TestBuildRejectsPathsOutsideProject(t *testing.T)

{
	p := &plugin{}
	raw, err := json.Marshal(struct {
		Dir  string `json:"dir"`
		Dest string `json:"dest"`
	}{Dir: "../articles", Dest: "build/static"})
	if err != nil {
		t.Fatal(err)
	}
	if err := p.Build(raw); err == nil {
		t.Fatal("expected source outside project to be rejected")
	}
}
S
struct

slugger

plugins/docs/slugger.go:13-15
type slugger struct

Methods

slug
Method

Parameters

text string

Returns

string
func (*slugger) slug(text string) string
{
	slug := strings.ToLower(text)
	slug = slugRe.ReplaceAllString(slug, "")
	slug = strings.TrimSpace(slug)
	slug = strings.ReplaceAll(slug, " ", "-")
	if n, ok := s.seen[slug]; ok {
		s.seen[slug] = n + 1
		return fmt.Sprintf("%s-%d", slug, n)
	}
	s.seen[slug] = 1
	return slug
}
slug
Method

Parameters

text string

Returns

string
func (*slugger) slug(text string) string
{
	slug := strings.ToLower(text)
	slug = slugRe.ReplaceAllString(slug, "")
	slug = strings.TrimSpace(slug)
	slug = strings.ReplaceAll(slug, " ", "-")
	if n, ok := s.seen[slug]; ok {
		s.seen[slug] = n + 1
		return fmt.Sprintf("%s-%d", slug, n)
	}
	s.seen[slug] = 1
	return slug
}

Fields

Name Type Description
seen map[string]int
F
function

newSlugger

Returns

plugins/docs/slugger.go:17-17
func newSlugger() *slugger

{ return &slugger{seen: make(map[string]int)} }
S
struct

slugger

plugins/docs/slugger_stub.go:13-15
type slugger struct

Methods

slug
Method

Parameters

text string

Returns

string
func (*slugger) slug(text string) string
{
	slug := strings.ToLower(text)
	slug = slugRe.ReplaceAllString(slug, "")
	slug = strings.TrimSpace(slug)
	slug = strings.ReplaceAll(slug, " ", "-")
	if n, ok := s.seen[slug]; ok {
		s.seen[slug] = n + 1
		return fmt.Sprintf("%s-%d", slug, n)
	}
	s.seen[slug] = 1
	return slug
}
slug
Method

Parameters

text string

Returns

string
func (*slugger) slug(text string) string
{
	slug := strings.ToLower(text)
	slug = slugRe.ReplaceAllString(slug, "")
	slug = strings.TrimSpace(slug)
	slug = strings.ReplaceAll(slug, " ", "-")
	if n, ok := s.seen[slug]; ok {
		s.seen[slug] = n + 1
		return fmt.Sprintf("%s-%d", slug, n)
	}
	s.seen[slug] = 1
	return slug
}

Fields

Name Type Description
seen map[string]int
F
function

newSlugger

Returns

plugins/docs/slugger_stub.go:17-17
func newSlugger() *slugger

{ return &slugger{seen: make(map[string]int)} }
S
struct

SidebarItem

SidebarItem describes one documentation navigation entry.

plugins/docs/docs.go:20-25
type SidebarItem struct

Fields

Name Type Description
Title string json:"title"
Path string json:"path"
Description string json:"description"
Children []SidebarItem json:"children"
S
struct

ArticleData

ArticleData contains a loaded documentation article.

plugins/docs/docs.go:28-32
type ArticleData struct

Fields

Name Type Description
Path string
Content string
Headings []Heading
S
struct

Heading

Heading describes one article heading.

plugins/docs/docs.go:35-39
type Heading struct

Fields

Name Type Description
Text string
Depth int
ID string
S
struct

Plugin

Plugin loads documentation navigation and article content.

plugins/docs/docs.go:42-48
type Plugin struct

Methods

Name
Method

Name returns the plugin name.

Returns

string
func (*Plugin) Name() string
{ return "docs" }
Optional
Method

Optional declares the SEO plugin when metadata support is enabled.

Returns

func (*Plugin) Optional() []core.Plugin
{
	if p.disableSEO {
		return nil
	}
	return []core.Plugin{seo.New()}
}
Provide
Method

Provide returns the values exposed to component templates.

Returns

map[string]any
func (*Plugin) Provide() map[string]any
{
	return map[string]any{
		"sidebar": p.sidebarData,
		"article": p.articleData,
		"loadDoc": p.loadArticle,
	}
}
Install
Method

Install loads the sidebar and exposes the article loader.

Parameters

func (*Plugin) Install(_ *core.App)
{
	for k, v := range p.Provide() {
		core.RegisterPluginVar("docs", k, v)
	}

	doc := js.Document()

	js.Fetch(p.Sidebar).Call("then", js.SafeFuncOf(func(_ js.Value, args []js.Value) any {
		res := args[0]
		res.Call("text").Call("then", js.SafeFuncOf(func(_ js.Value, args []js.Value) any {
			raw := args[0].String()
			var items []SidebarItem
			if err := json.Unmarshal([]byte(raw), &items); err == nil {
				p.sidebarData.Set(items)
			}
			js.Set("__rfwDocsSidebar", raw)
			doc.Call("dispatchEvent", js.CustomEvent().New("rfwSidebar"))
			events.EmitApp(events.EventSidebarLoaded, items)
			return nil
		}))
		return nil
	}))

	p.loader = js.SafeFuncOf(func(_ js.Value, args []js.Value) any {
		if len(args) < 1 {
			return nil
		}
		p.loadArticle(args[0].String())
		return nil
	})
	js.Set("rfwLoadDoc", p.loader)
}
loadArticle
Method

Parameters

path string
func (*Plugin) loadArticle(path string)
{
	js.Fetch(path).Call("then", js.SafeFuncOf(func(_ js.Value, args []js.Value) any {
		res := args[0]
		res.Call("text").Call("then", js.SafeFuncOf(func(_ js.Value, args []js.Value) any {
			content := args[0].String()
			mhs := markdown.Headings(content)
			headings := make([]Heading, len(mhs))
			for i, h := range mhs {
				headings[i] = Heading{Text: h.Text, Depth: h.Depth, ID: h.ID}
			}
			data := &ArticleData{
				Path:     path,
				Content:  content,
				Headings: headings,
			}
			p.articleData.Set(data)

			doc := js.Document()
			doc.Call("dispatchEvent", js.CustomEvent().New("rfwDoc", map[string]any{
				"detail": map[string]any{
					"path":     path,
					"content":  content,
					"headings": headingsToAny(headings),
				},
			}))
			events.EmitApp(events.EventArticleLoaded, data)
			return nil
		}))
		return nil
	}))
}
Build
Method

Build accepts the plugin build configuration.

Parameters

Returns

error
func (*Plugin) Build(json.RawMessage) error
{ return nil }

Fields

Name Type Description
Sidebar string
disableSEO bool
loader js.Func
sidebarData *state.Signal[[]SidebarItem]
articleData *state.Signal[*ArticleData]
F
function

New

New creates a documentation plugin using the supplied sidebar URL.

Parameters

sidebar
string
disableSEO
...bool

Returns

plugins/docs/docs.go:51-60
func New(sidebar string, disableSEO ...bool) *Plugin

{
	sidebar = fmt.Sprintf("%s?%d", sidebar, time.Now().Unix())
	p := &Plugin{Sidebar: sidebar}
	if len(disableSEO) > 0 {
		p.disableSEO = disableSEO[0]
	}
	p.sidebarData = state.NewSignal[[]SidebarItem](nil)
	p.articleData = state.NewSignal[*ArticleData](nil)
	return p
}
F
function

headingsToAny

Parameters

headings

Returns

[]any
plugins/docs/docs.go:148-158
func headingsToAny(headings []Heading) []any

{
	result := make([]any, len(headings))
	for i, h := range headings {
		result[i] = map[string]any{
			"text":  h.Text,
			"depth": h.Depth,
			"id":    h.ID,
		}
	}
	return result
}
F
function

TestSlugger

TestSlugger ensures slug generation is deterministic and handles duplicates.

Parameters

plugins/docs/docs_test.go:6-16
func TestSlugger(t *testing.T)

{
	s := newSlugger()
	first := s.slug("Hello World!")
	if first != "hello-world" {
		t.Fatalf("expected 'hello-world', got %q", first)
	}
	second := s.slug("Hello World!")
	if second != "hello-world-1" {
		t.Fatalf("expected 'hello-world-1', got %q", second)
	}
}
F
function

LoadArticle

LoadArticle fetches and renders the markdown document at the given path.
It relies on the rfwLoadDoc loader injected by the docs plugin and should
be used instead of direct js.Call invocations.

Parameters

path
string
plugins/docs/loader.go:10-12
func LoadArticle(path string)

{
	js.Call("rfwLoadDoc", path)
}
F
function

LoadArticle

LoadArticle is a no-op when not running in a js/wasm environment.

Parameters

string
plugins/docs/loader_stub.go:7-7
func LoadArticle(string)

{}
F
function

TestPluginProviderAndOptional

Parameters

plugins/docs/provider_test.go:11-32
func TestPluginProviderAndOptional(t *testing.T)

{
	p := New("/sidebar.json")
	provided := p.Provide()

	if _, ok := provided["sidebar"].(*state.Signal[[]SidebarItem]); !ok {
		t.Fatalf("expected sidebar signal provider, got %T", provided["sidebar"])
	}
	if _, ok := provided["article"].(*state.Signal[*ArticleData]); !ok {
		t.Fatalf("expected article signal provider, got %T", provided["article"])
	}
	if _, ok := provided["loadDoc"].(func(string)); !ok {
		t.Fatalf("expected loadDoc function provider, got %T", provided["loadDoc"])
	}
	if len(p.Optional()) != 1 {
		t.Fatalf("expected SEO optional plugin by default")
	}

	withoutSEO := New("/sidebar.json", true)
	if optional := withoutSEO.Optional(); optional != nil {
		t.Fatalf("expected nil optional plugins when SEO disabled, got %v", optional)
	}
}
F
function

TestHeadingsToAny

Parameters

plugins/docs/provider_test.go:34-46
func TestHeadingsToAny(t *testing.T)

{
	out := headingsToAny([]Heading{{Text: "Intro", Depth: 2, ID: "intro"}})
	if len(out) != 1 {
		t.Fatalf("expected one heading, got %d", len(out))
	}
	m, ok := out[0].(map[string]any)
	if !ok {
		t.Fatalf("expected map heading, got %T", out[0])
	}
	if m["text"] != "Intro" || m["depth"] != 2 || m["id"] != "intro" {
		t.Fatalf("unexpected heading map: %v", m)
	}
}