Skip to content

Commit e57fef1

Browse files
authored
feat(page): normalize the authoring API — positional Header title, prose/example helpers (#27)
## Summary One consistent convention across the kit: the primary argument is positional, modifiers are keyword arguments — and the everyday authoring path no longer hits the Ruby parens-with-blocks SyntaxError. - `DocsUI::Header` takes the title positionally (`Header("Installation")`), matching Section/Code. The legacy `title:` kwarg still works (silent compat, no deprecation); the positional wins if both are given. - New lowercase, block-friendly page helpers `prose { … }` and `example { |ex| … }` (alongside the existing `md`), extracted into `DocsUI::PageHelpers` and mixed into `DocsUI::Page`. A lowercase method takes a block without parens, so the gotcha structurally cannot occur. The kit forms `DocsUI::Prose()` / `DocsUI::Example()` stay valid forever. - The gem's own docs pages adopt the helpers; the dedicated "parens gotcha" section shrinks to a footnote. README documents the convention. `PageHelpers` is a module (not methods inline on Page) so the helpers are unit-testable against a bare Phlex host — `DocsUI::Page` includes Phlex::Rails::Helpers::Routes and cannot load in the standalone suite. ## Test Coverage - spec/docs_ui/header_spec.rb: positional title, eyebrow, lead block; legacy `title:` kwarg renders identically (backwards-compat proof); positional wins when both given. - spec/docs_ui/page_helpers_spec.rb: `prose`/`example`/`md` render the expected Prose wrapper / Example tabs / Markdown against a bare Phlex host exercising the REAL PageHelpers module. - spec/docs_ui/markdown_spec.rb: the `md` delegation test now includes the real PageHelpers module instead of re-implementing `md`. ## Verification - [x] bundle exec rake (rspec 103 examples, 0 failures; rubocop 42 files clean) - [x] header.rb + page_helpers.rb: 100% line coverage - [x] grep gate: no live DocsUI::Prose()/Example() in gem docs (only code{} API references remain) - [x] backwards compatible: `Header(title:)` covered by spec; existing consumer sites unchanged Refs #10
1 parent 1c76915 commit e57fef1

16 files changed

Lines changed: 325 additions & 120 deletions

File tree

README.md

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -91,16 +91,47 @@ class Views::Docs::Pages::Installation < DocsUI::Page
9191
def lead = "Add the gem and render your first component."
9292

9393
def content
94-
render DocsUI::Section.new("Add the gem") do
95-
render DocsUI::Prose.new { p { "Components are plain Ruby classes." } }
96-
render DocsUI::Code.new(<<~RUBY, filename: "Gemfile")
94+
DocsUI::Section("Add the gem") do
95+
prose { p { "Components are plain Ruby classes." } }
96+
DocsUI::Code(<<~RUBY, filename: "Gemfile")
9797
gem "docs-kit"
9898
RUBY
9999
end
100100
end
101101
end
102102
```
103103

104+
`DocsUI::Page` includes the kit, so inside `#content` you call the components
105+
directly — `DocsUI::Section(...)`, `DocsUI::Code(...)` — no `render … .new`.
106+
107+
### The authoring convention
108+
109+
One rule covers the whole kit: **the primary argument is positional; modifiers
110+
are keyword arguments.**
111+
112+
```ruby
113+
DocsUI::Header("Installation", eyebrow: "Guide") # title positional
114+
DocsUI::Section("Add the gem", id: "add", description: …) # title positional
115+
DocsUI::Code(source, lexer: :ruby, filename: "Gemfile") # source positional
116+
```
117+
118+
For the two wrappers that take **no** positional argument — prose and a
119+
multi-language example — `DocsUI::Page` gives you lowercase helpers so a block
120+
needs no parens:
121+
122+
```ruby
123+
prose { p { "Hand-authored prose." } } # → DocsUI::Prose
124+
example { |ex| ex.code(:ruby) { source } } # → DocsUI::Example
125+
md(<<~'MD') # → DocsUI::Markdown
126+
A block of **Markdown**.
127+
MD
128+
```
129+
130+
The kit forms `DocsUI::Prose() { … }` / `DocsUI::Example() { … }` still work —
131+
they just need the empty `()`, because a bare `DocsUI::Prose do … end` parses as
132+
a constant reference (a Ruby `SyntaxError`). The lowercase helpers sidestep that
133+
entirely, so they're the everyday path.
134+
104135
## Authoring with Markdown
105136

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

139170
Two things to know:
140171

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

app/components/docs_ui/header.rb

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,18 @@ module DocsUI
44
# A doc page header: an optional eyebrow (kicker), the title, and a lead
55
# paragraph. Gives every doc page a consistent masthead.
66
#
7-
# render DocsUI::Header.new(title: "Installation", eyebrow: "Guide") do
7+
# render DocsUI::Header.new("Installation", eyebrow: "Guide") do
88
# plain "Add the gem and render your first component."
99
# end
10+
#
11+
# The primary argument (the title) is positional, matching Section/Code and the
12+
# kit-wide convention. The legacy `title:` kwarg still works so existing sites
13+
# keep rendering unchanged; the positional wins if both are given.
1014
class Header < Phlex::HTML
11-
def initialize(title:, eyebrow: nil)
12-
@title = title
15+
# Positional title (the convention), with a silent `title:` kwarg fallback for
16+
# sites that still pass it by keyword. Positional wins when both are given.
17+
def initialize(title = nil, eyebrow: nil, **opts)
18+
@title = title || opts[:title]
1319
@eyebrow = eyebrow
1420
end
1521

app/components/docs_ui/page.rb

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ class Page < Phlex::HTML
1919
# Authored pages subclass this, so include the kit here: a page body can call
2020
# DocsUI::Section(...) / DocsUI::Code(...) directly, no render ... .new.
2121
include DocsUI
22+
# The lowercase, block-friendly authoring helpers (md/prose/example) — the
23+
# friction-free path that never trips the parens-with-blocks gotcha.
24+
include DocsUI::PageHelpers
2225

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

49-
render DocsUI::Header.new(title: self.class.title, eyebrow: self.class.eyebrow) do
52+
render DocsUI::Header.new(self.class.title, eyebrow: self.class.eyebrow) do
5053
plain lead if lead
5154
end
5255

5356
content
5457
end
5558
end
5659

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

6664
# Override in subclasses for the lead paragraph (optional).
6765
def lead = nil
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# frozen_string_literal: true
2+
3+
module DocsUI
4+
# Lowercase, block-friendly authoring helpers mixed into DocsUI::Page. They
5+
# exist so the everyday page body never trips the Ruby parens-with-blocks trap:
6+
# a lowercase method call takes a block WITHOUT parens, so `prose do … end` is
7+
# unambiguously a method call (the bare `DocsUI::Prose do … end` kit form parses
8+
# as a constant reference — a SyntaxError). The kit forms stay valid; these are
9+
# the friction-free path.
10+
#
11+
# Extracted from Page so they can be unit-tested against a bare Phlex host:
12+
# Page itself includes Phlex::Rails::Helpers::Routes (a live Rails view context)
13+
# and cannot load in the standalone suite.
14+
module PageHelpers
15+
# Render a block of GFM Markdown as Prose-styled prose (see DocsUI::Markdown).
16+
# A lowercase method + heredoc sidesteps the parens-with-blocks gotcha:
17+
# md <<~'MD'
18+
# Write **prose** as Markdown. Single-quoted heredoc so #{} stays literal.
19+
# MD
20+
def md(source)
21+
render DocsUI::Markdown.new(source)
22+
end
23+
24+
# Render hand-authored prose in a DocsUI::Prose wrapper. Lowercase, so it
25+
# takes the block without parens: `prose do p { "…" } end`.
26+
def prose(&)
27+
render DocsUI::Prose.new(&)
28+
end
29+
30+
# Render a multi-language code group (DocsUI::Example). Lowercase, so it takes
31+
# the block without parens: `example do |ex| ex.code(:ruby) { … } end`.
32+
def example(&)
33+
render DocsUI::Example.new(&)
34+
end
35+
end
36+
end

docs/app/views/docs/pages/authoring.rb

Lines changed: 43 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,8 @@
33
module Views
44
module Docs
55
module Pages
6-
# How to write a documentation page: a Phlex class, a registry entry, and
7-
# the DocsUI building blocks. Also covers the parens-with-blocks gotcha and
8-
# the automatic "On this page" TOC.
6+
# How to write a documentation page: a Phlex class, a registry entry, and
7+
# the DocsUI building blocks, plus the automatic "On this page" TOC.
98
class Authoring < DocsUI::Page
109
title "Authoring pages"
1110
eyebrow "Getting started"
@@ -16,7 +15,6 @@ def content
1615
page_is_a_class_section
1716
register_section
1817
building_blocks_section
19-
parens_gotcha_section
2018
toc_section
2119
end
2220

@@ -39,7 +37,7 @@ def lead = "One sentence that sits under the page title."
3937
4038
def content
4139
DocsUI::Section("First steps", description: "What this section covers.") do
42-
DocsUI::Prose() do
40+
prose do
4341
p { "Hand-authored prose with consistent reading rhythm." }
4442
end
4543
@@ -56,7 +54,7 @@ def content
5654
end
5755
RUBY
5856

59-
DocsUI::Prose() do
57+
prose do
6058
p do
6159
code { "title" }
6260
plain " names the page, "
@@ -79,7 +77,7 @@ def content
7977
def register_section
8078
DocsUI::Section("Register the page",
8179
description: "Add an entry so it appears in the nav and resolves at /docs/<slug>.") do
82-
DocsUI::Prose() do
80+
prose do
8381
p do
8482
plain "A page shows up once it has a row in the "
8583
code { "Doc" }
@@ -116,48 +114,47 @@ def building_blocks_section
116114
render PropTable.new(
117115
[ "Helper", "Use for" ],
118116
[
119-
[ "DocsUI::Section", "an anchored subsection with a heading (+ optional description)" ],
120-
[ "DocsUI::Prose()", "hand-authored prose (needs parens with a block)" ],
121-
[ "DocsUI::Code", "a syntax-highlighted code block" ],
122-
[ "DocsUI::Example()", "multi-language tabbed code" ],
123-
[ "DocsUI::Callout", "note / tip / warning boxes" ]
117+
[ "DocsUI::Section(title)", "an anchored subsection with a heading (+ optional description)" ],
118+
[ "md(source)", "a block of GFM Markdown, styled like Prose" ],
119+
[ "prose { … }", "hand-authored prose (p/ul/code) in a reading-rhythm wrapper" ],
120+
[ "DocsUI::Code(source)", "a syntax-highlighted code block" ],
121+
[ "example { |ex| … }", "multi-language tabbed code" ],
122+
[ "DocsUI::Callout(level)", "note / tip / warning boxes" ]
124123
]
125124
)
126-
end
127-
end
128-
129-
def parens_gotcha_section
130-
DocsUI::Section("Gotcha: parens with blocks",
131-
description: "The one syntax rule that bites everyone.") do
132-
DocsUI::Callout(:warning) do
133-
"DocsUI::Prose and DocsUI::Example take no positional args, so with a block you MUST write " \
134-
"DocsUI::Prose() do … end. The bare form parses as a constant reference — a Ruby SyntaxError."
135-
end
136-
137-
DocsUI::Code(<<~RUBY)
138-
# Wrong — SyntaxError: `do` block reads as a constant reference.
139-
DocsUI::Prose do
140-
p { "..." }
141-
end
142-
143-
# Right — the parens make it a method call that takes the block.
144-
DocsUI::Prose() do
145-
p { "..." }
146-
end
147-
RUBY
148125

149-
DocsUI::Prose() do
126+
prose do
150127
p do
151-
code { "DocsUI::Section" }
128+
plain "The primary argument is always positional — "
129+
code { "Section(\"Title\")" }
152130
plain ", "
153-
code { "DocsUI::Code" }
154-
plain ", and "
155-
code { "DocsUI::Callout" }
156-
plain " already take arguments, so their parens are never optional — the gotcha is only "
157-
code { "Prose" }
158-
plain " and "
159-
code { "Example" }
160-
plain "."
131+
code { "Code(source)" }
132+
plain ", "
133+
code { "Header(\"Title\")" }
134+
plain " — with modifiers as keywords ("
135+
code { "description:" }
136+
plain ", "
137+
code { "eyebrow:" }
138+
plain ")."
139+
end
140+
p do
141+
plain "For the wrappers that take no argument, use the lowercase page helpers "
142+
code { "prose" }
143+
plain " / "
144+
code { "example" }
145+
plain " (and "
146+
code { "md" }
147+
plain " for Markdown). A lowercase method takes a block without parens, so "
148+
code { "prose do … end" }
149+
plain " just works. The kit forms "
150+
code { "DocsUI::Prose()" }
151+
plain " / "
152+
code { "DocsUI::Example()" }
153+
plain " stay valid — they only need the empty "
154+
code { "()" }
155+
plain " because a bare "
156+
code { "DocsUI::Prose do" }
157+
plain " parses as a constant reference (a SyntaxError)."
161158
end
162159
end
163160
end
@@ -166,7 +163,7 @@ def parens_gotcha_section
166163
def toc_section
167164
DocsUI::Section("The \"On this page\" TOC",
168165
description: "Built for you from your section headings.") do
169-
DocsUI::Prose() do
166+
prose do
170167
p do
171168
plain "Every "
172169
code { "DocsUI::Section" }
@@ -187,7 +184,7 @@ class Views::Docs::Pages::Guide < DocsUI::Page
187184
end
188185
RUBY
189186

190-
DocsUI::Prose() do
187+
prose do
191188
p do
192189
plain "See the "
193190
a(href: "/docs/on-this-page") { "On this page" }

0 commit comments

Comments
 (0)