Skip to content
Merged
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
42 changes: 36 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,16 +91,47 @@ class Views::Docs::Pages::Installation < DocsUI::Page
def lead = "Add the gem and render your first component."

def content
render DocsUI::Section.new("Add the gem") do
render DocsUI::Prose.new { p { "Components are plain Ruby classes." } }
render DocsUI::Code.new(<<~RUBY, filename: "Gemfile")
DocsUI::Section("Add the gem") do
prose { p { "Components are plain Ruby classes." } }
DocsUI::Code(<<~RUBY, filename: "Gemfile")
gem "docs-kit"
RUBY
end
end
end
```

`DocsUI::Page` includes the kit, so inside `#content` you call the components
directly — `DocsUI::Section(...)`, `DocsUI::Code(...)` — no `render … .new`.

### The authoring convention

One rule covers the whole kit: **the primary argument is positional; modifiers
are keyword arguments.**

```ruby
DocsUI::Header("Installation", eyebrow: "Guide") # title positional
DocsUI::Section("Add the gem", id: "add", description: …) # title positional
DocsUI::Code(source, lexer: :ruby, filename: "Gemfile") # source positional
```

For the two wrappers that take **no** positional argument — prose and a
multi-language example — `DocsUI::Page` gives you lowercase helpers so a block
needs no parens:

```ruby
prose { p { "Hand-authored prose." } } # → DocsUI::Prose
example { |ex| ex.code(:ruby) { source } } # → DocsUI::Example
md(<<~'MD') # → DocsUI::Markdown
A block of **Markdown**.
MD
```

The kit forms `DocsUI::Prose() { … }` / `DocsUI::Example() { … }` still work —
they just need the empty `()`, because a bare `DocsUI::Prose do … end` parses as
a constant reference (a Ruby `SyntaxError`). The lowercase helpers sidestep that
entirely, so they're the everyday path.

## Authoring with Markdown

Prose is the most-written content type — and the noisiest to hand-build from
Expand Down Expand Up @@ -138,9 +169,8 @@ hand-written `DocsUI::Code`; an unknown fence language falls back to plaintext.

Two things to know:

- **`md` is a lowercase method, so `md <<~MD … MD` needs no parens** — unlike
`DocsUI::Prose()` / `DocsUI::Example()`, which take a block and so require the
empty-parens form.
- **`md` is a lowercase page helper (like `prose`/`example`), so `md <<~MD … MD`
needs no parens** — see [the authoring convention](#the-authoring-convention).
- **Use a single-quoted heredoc, `<<~'MD'`.** Then `#{…}` in your prose is
literal text (Phlex escapes author text — no `html_safe`, no interpolation).

Expand Down
12 changes: 9 additions & 3 deletions app/components/docs_ui/header.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,18 @@ module DocsUI
# A doc page header: an optional eyebrow (kicker), the title, and a lead
# paragraph. Gives every doc page a consistent masthead.
#
# render DocsUI::Header.new(title: "Installation", eyebrow: "Guide") do
# render DocsUI::Header.new("Installation", eyebrow: "Guide") do
# plain "Add the gem and render your first component."
# end
#
# The primary argument (the title) is positional, matching Section/Code and the
# kit-wide convention. The legacy `title:` kwarg still works so existing sites
# keep rendering unchanged; the positional wins if both are given.
class Header < Phlex::HTML
def initialize(title:, eyebrow: nil)
@title = title
# Positional title (the convention), with a silent `title:` kwarg fallback for
# sites that still pass it by keyword. Positional wins when both are given.
def initialize(title = nil, eyebrow: nil, **opts)
@title = title || opts[:title]
@eyebrow = eyebrow
end

Expand Down
16 changes: 7 additions & 9 deletions app/components/docs_ui/page.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ class Page < Phlex::HTML
# Authored pages subclass this, so include the kit here: a page body can call
# DocsUI::Section(...) / DocsUI::Code(...) directly, no render ... .new.
include DocsUI
# The lowercase, block-friendly authoring helpers (md/prose/example) — the
# friction-free path that never trips the parens-with-blocks gotcha.
include DocsUI::PageHelpers

class << self
def title(value = nil)
Expand Down Expand Up @@ -46,22 +49,17 @@ def view_template
a(href: root_path, class: "link link-hover text-sm opacity-70") { "← Home" }
end

render DocsUI::Header.new(title: self.class.title, eyebrow: self.class.eyebrow) do
render DocsUI::Header.new(self.class.title, eyebrow: self.class.eyebrow) do
plain lead if lead
end

content
end
end

# Render a block of GFM Markdown as Prose-styled prose (see DocsUI::Markdown).
# A lowercase method + heredoc sidesteps the parens-with-blocks gotcha:
# md <<~'MD'
# Write **prose** as Markdown. Single-quoted heredoc so #{} stays literal.
# MD
def md(source)
render DocsUI::Markdown.new(source)
end
# The lowercase authoring helpers md/prose/example come from DocsUI::PageHelpers
# (included above) — the parens-free path that never hits the constant-reference
# SyntaxError. The kit forms (DocsUI::Prose(), DocsUI::Example()) stay valid too.

# Override in subclasses for the lead paragraph (optional).
def lead = nil
Expand Down
36 changes: 36 additions & 0 deletions app/components/docs_ui/page_helpers.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# frozen_string_literal: true

module DocsUI
# Lowercase, block-friendly authoring helpers mixed into DocsUI::Page. They
# exist so the everyday page body never trips the Ruby parens-with-blocks trap:
# a lowercase method call takes a block WITHOUT parens, so `prose do … end` is
# unambiguously a method call (the bare `DocsUI::Prose do … end` kit form parses
# as a constant reference — a SyntaxError). The kit forms stay valid; these are
# the friction-free path.
#
# Extracted from Page so they can be unit-tested against a bare Phlex host:
# Page itself includes Phlex::Rails::Helpers::Routes (a live Rails view context)
# and cannot load in the standalone suite.
module PageHelpers
# Render a block of GFM Markdown as Prose-styled prose (see DocsUI::Markdown).
# A lowercase method + heredoc sidesteps the parens-with-blocks gotcha:
# md <<~'MD'
# Write **prose** as Markdown. Single-quoted heredoc so #{} stays literal.
# MD
def md(source)
render DocsUI::Markdown.new(source)
end

# Render hand-authored prose in a DocsUI::Prose wrapper. Lowercase, so it
# takes the block without parens: `prose do p { "…" } end`.
def prose(&)
render DocsUI::Prose.new(&)
end

# Render a multi-language code group (DocsUI::Example). Lowercase, so it takes
# the block without parens: `example do |ex| ex.code(:ruby) { … } end`.
def example(&)
render DocsUI::Example.new(&)
end
end
end
89 changes: 43 additions & 46 deletions docs/app/views/docs/pages/authoring.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,8 @@
module Views
module Docs
module Pages
# How to write a documentation page: a Phlex class, a registry entry, and
# the DocsUI building blocks. Also covers the parens-with-blocks gotcha and
# the automatic "On this page" TOC.
# How to write a documentation page: a Phlex class, a registry entry, and
# the DocsUI building blocks, plus the automatic "On this page" TOC.
class Authoring < DocsUI::Page
title "Authoring pages"
eyebrow "Getting started"
Expand All @@ -16,7 +15,6 @@ def content
page_is_a_class_section
register_section
building_blocks_section
parens_gotcha_section
toc_section
end

Expand All @@ -39,7 +37,7 @@ def lead = "One sentence that sits under the page title."

def content
DocsUI::Section("First steps", description: "What this section covers.") do
DocsUI::Prose() do
prose do
p { "Hand-authored prose with consistent reading rhythm." }
end

Expand All @@ -56,7 +54,7 @@ def content
end
RUBY

DocsUI::Prose() do
prose do
p do
code { "title" }
plain " names the page, "
Expand All @@ -79,7 +77,7 @@ def content
def register_section
DocsUI::Section("Register the page",
description: "Add an entry so it appears in the nav and resolves at /docs/<slug>.") do
DocsUI::Prose() do
prose do
p do
plain "A page shows up once it has a row in the "
code { "Doc" }
Expand Down Expand Up @@ -116,48 +114,47 @@ def building_blocks_section
render PropTable.new(
[ "Helper", "Use for" ],
[
[ "DocsUI::Section", "an anchored subsection with a heading (+ optional description)" ],
[ "DocsUI::Prose()", "hand-authored prose (needs parens with a block)" ],
[ "DocsUI::Code", "a syntax-highlighted code block" ],
[ "DocsUI::Example()", "multi-language tabbed code" ],
[ "DocsUI::Callout", "note / tip / warning boxes" ]
[ "DocsUI::Section(title)", "an anchored subsection with a heading (+ optional description)" ],
[ "md(source)", "a block of GFM Markdown, styled like Prose" ],
[ "prose { … }", "hand-authored prose (p/ul/code) in a reading-rhythm wrapper" ],
[ "DocsUI::Code(source)", "a syntax-highlighted code block" ],
[ "example { |ex| … }", "multi-language tabbed code" ],
[ "DocsUI::Callout(level)", "note / tip / warning boxes" ]
]
)
end
end

def parens_gotcha_section
DocsUI::Section("Gotcha: parens with blocks",
description: "The one syntax rule that bites everyone.") do
DocsUI::Callout(:warning) do
"DocsUI::Prose and DocsUI::Example take no positional args, so with a block you MUST write " \
"DocsUI::Prose() do … end. The bare form parses as a constant reference — a Ruby SyntaxError."
end

DocsUI::Code(<<~RUBY)
# Wrong — SyntaxError: `do` block reads as a constant reference.
DocsUI::Prose do
p { "..." }
end

# Right — the parens make it a method call that takes the block.
DocsUI::Prose() do
p { "..." }
end
RUBY

DocsUI::Prose() do
prose do
p do
code { "DocsUI::Section" }
plain "The primary argument is always positional — "
code { "Section(\"Title\")" }
plain ", "
code { "DocsUI::Code" }
plain ", and "
code { "DocsUI::Callout" }
plain " already take arguments, so their parens are never optional — the gotcha is only "
code { "Prose" }
plain " and "
code { "Example" }
plain "."
code { "Code(source)" }
plain ", "
code { "Header(\"Title\")" }
plain " — with modifiers as keywords ("
code { "description:" }
plain ", "
code { "eyebrow:" }
plain ")."
end
p do
plain "For the wrappers that take no argument, use the lowercase page helpers "
code { "prose" }
plain " / "
code { "example" }
plain " (and "
code { "md" }
plain " for Markdown). A lowercase method takes a block without parens, so "
code { "prose do … end" }
plain " just works. The kit forms "
code { "DocsUI::Prose()" }
plain " / "
code { "DocsUI::Example()" }
plain " stay valid — they only need the empty "
code { "()" }
plain " because a bare "
code { "DocsUI::Prose do" }
plain " parses as a constant reference (a SyntaxError)."
end
end
end
Expand All @@ -166,7 +163,7 @@ def parens_gotcha_section
def toc_section
DocsUI::Section("The \"On this page\" TOC",
description: "Built for you from your section headings.") do
DocsUI::Prose() do
prose do
p do
plain "Every "
code { "DocsUI::Section" }
Expand All @@ -187,7 +184,7 @@ class Views::Docs::Pages::Guide < DocsUI::Page
end
RUBY

DocsUI::Prose() do
prose do
p do
plain "See the "
a(href: "/docs/on-this-page") { "On this page" }
Expand Down
Loading
Loading