Skip to content
Open
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
87 changes: 87 additions & 0 deletions docs/go/carousel.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package dangdocs

import (
"fmt"
"strings"

"github.com/vito/booklit"
"github.com/vito/dang/v2/pkg/dang"
)

// DangCarousel renders a feature carousel: a strip of \dang-feature slides,
// each a titled, distinctive-feature showcase. docs/js/carousel.js
// progressively enhances it into a tabbed, arrow-navigated reel; without
// JavaScript every slide renders stacked and fully readable.
//
// \dang-carousel{
// \dang-feature{Prototype objects}{{{
// ...code...
// }}}
// }{
// \dang-feature{Copy-on-write}{{{
// ...code...
// }}}
// }
func (p Plugin) DangCarousel(slides ...booklit.Content) booklit.Content {
return booklit.Styled{
Style: "carousel",
Content: booklit.Sequence(slides),
Block: true,
}
}

// DangFeature renders one carousel slide: a feature title above a Dang snippet
// rendered exactly as a \dang-literate block — same template, styling, baked
// output, and client-side editor (see literate.go / docs/js/playground.js), so
// the carousel reuses the site's one code-snippet mechanism rather than
// inventing another.
//
// The one difference from a page literate block is isolation: each slide
// evaluates in its own fresh standard-library session (not the page-shared
// chain), since slides are independent showcases that each (re)declare their
// own types. The slide wrapper carries data-dang-literate-chain so playground.js
// replays it as a standalone chain client-side too. As with any literate block
// the snippet is evaluated at build time and a parse/type/eval failure fails
// the docs build, so the examples can't rot.
//
// \dang-feature{Prototype objects}{{{
// type Greeter { name: String! greet: String! { `hi ${name}` } }
// Greeter("world").greet
// }}}
func (p Plugin) DangFeature(title, code booklit.Content) (booklit.Content, error) {
source := strings.TrimRight(code.String(), "\n")

// A fresh session per slide keeps slides independent — one slide's
// declarations must not leak into the next.
typeScope, valueScope := dang.BuildScopesFromImports("", nil)
sess := &literateSession{typeScope: typeScope, valueScope: valueScope}

stdout, value, err := literateEval(source, sess)
if err != nil {
return nil, fmt.Errorf(`\dang-feature %q in %s: %w`, title.String(), p.section.FilePath(), err)
}

// Build the same Styled "dang-literate" content a \dang-literate block
// produces (literate.go's literateBlock), so it renders through
// dang-literate.tmpl identically.
partials := booklit.Partials{}
if stdout != "" {
partials["Stdout"] = booklit.String(stdout)
}
if value != "" {
partials["Value"] = highlightResult(value)
}
literate := booklit.Styled{
Style: "dang-literate",
Content: p.highlightDang(source),
Partials: partials,
Block: true,
}

return booklit.Styled{
Style: "carousel-slide",
Content: literate,
Partials: booklit.Partials{"Title": title},
Block: true,
}, nil
}
4 changes: 4 additions & 0 deletions docs/html/carousel-slide.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<div class="dang-carousel-slide" data-dang-carousel-slide data-dang-literate-chain>
<div class="dang-carousel-title">{{.Partial "Title" | render}}</div>
{{.Content | render}}
</div>
1 change: 1 addition & 0 deletions docs/html/carousel.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<div class="dang-carousel" data-dang-carousel>{{.Content | render}}</div>
54 changes: 54 additions & 0 deletions docs/html/page.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,59 @@ pre code{display:block}
.dang-literate:focus-within .dang-literate-controls{opacity:1}
@media (hover:none){.dang-literate-controls{opacity:1}}

/* ── feature carousel (carousel.js) ──────────────────────────────────── */
/* A showcase reel of distinctive features; each \dang-feature slide is a
titled, build-evaluated snippet. carousel.js adds .is-enhanced, which is
what collapses the stacked slides into a tabbed, arrow-navigated panel —
without JS every slide just renders in sequence, fully readable. */
.dang-carousel{
border:1px solid var(--code-border);border-radius:8px;margin:1.5rem 0;
background:var(--bg2);overflow:hidden;
}
/* Tab strip of feature names, built by carousel.js — the menu of "what's
different", and the primary jump-to-slide control. Scrolls sideways rather
than wrapping on a narrow viewport so it never towers over the code. */
.dang-carousel-tabs{
display:flex;gap:.25rem;padding:.5rem .5rem 0;overflow-x:auto;
border-bottom:1px solid var(--code-border);background:var(--bg2);
}
.dang-carousel-tab{
font-family:'Inter',system-ui,sans-serif;font-size:.76rem;font-weight:500;cursor:pointer;
border:1px solid transparent;border-bottom:none;background:transparent;color:var(--fg2);
padding:.35rem .65rem;border-radius:6px 6px 0 0;line-height:1.3;white-space:nowrap;
transition:color .12s,background .12s;
}
.dang-carousel-tab:hover{color:var(--fg);background:var(--bg3)}
.dang-carousel-tab.is-active{color:var(--accent2);background:var(--code-bg);border-color:var(--code-border)}
/* Enhanced: only the active slide is shown. */
.dang-carousel.is-enhanced .dang-carousel-slide{display:none}
.dang-carousel.is-enhanced .dang-carousel-slide.is-active{display:block}
/* Unenhanced fallback: stack the slides with a divider between them. */
.dang-carousel-slide + .dang-carousel-slide{border-top:1px solid var(--code-border)}
.dang-carousel-title{
font-size:.82rem;font-weight:600;color:var(--fg);
padding:.6rem .9rem .1rem;
}
/* Each slide embeds a real \dang-literate block; strip its standalone framing
so it sits flush inside the carousel rather than as a bordered card. */
.dang-carousel-slide .dang-literate{margin:0;border:none;border-radius:0;background:var(--code-bg)}
/* Prev / counter / next, built by carousel.js. */
.dang-carousel-foot{
display:flex;align-items:center;justify-content:flex-end;gap:.5rem;
padding:.4rem .6rem;background:var(--bg2);border-top:1px solid var(--code-border);
}
.dang-carousel-arrow{
font-family:'Inter',system-ui,sans-serif;font-size:.9rem;cursor:pointer;
border:1px solid var(--code-border);background:var(--bg3);color:var(--fg);
width:1.7rem;height:1.7rem;border-radius:5px;line-height:1;
display:flex;align-items:center;justify-content:center;transition:border-color .12s,color .12s;
}
.dang-carousel-arrow:hover{border-color:var(--fg2);color:var(--accent2)}
.dang-carousel-count{
font-size:.74rem;color:var(--fg2);font-family:'JetBrains Mono',monospace;
min-width:3rem;text-align:center;
}

table{width:100%;border-collapse:collapse;font-size:.85rem;margin:.75rem 0}
th{text-align:left;padding:.5rem .6rem;color:var(--fg2);font-weight:500;border-bottom:1px solid var(--code-border)}
td{padding:.4rem .6rem;border-bottom:1px solid var(--td-border)}
Expand Down Expand Up @@ -366,5 +419,6 @@ footer{margin-top:4rem;padding-top:1.5rem;border-top:1px solid var(--code-border
<script src="js/search.js" defer></script>
<script src="js/feedback.js" defer></script>
<script src="js/playground.js" defer></script>
<script src="js/carousel.js" defer></script>
</body>
</html>
119 changes: 119 additions & 0 deletions docs/js/carousel.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
// carousel.js — progressive enhancement for \dang-carousel blocks.
//
// Each carousel is authored as a stack of slides:
//
// <div class="dang-carousel" data-dang-carousel>
// <div class="dang-carousel-slide" data-dang-carousel-slide>
// <div class="dang-carousel-title">Prototype objects</div>
// <div class="dang-carousel-code">…baked snippet + output…</div>
// </div>
// …more slides…
// </div>
//
// Without JS the reader sees every slide in sequence — a plain list of
// examples. With JS we add a tab strip of feature names (jump to any slide), a
// prev/counter/next footer, and arrow-key navigation, and show one slide at a
// time. The snippets are baked at build time (docs/go/carousel.go), so nothing
// here loads or evaluates Dang — this is pure presentation.

(function () {
"use strict";

function enhance(carousel) {
var slides = Array.prototype.slice.call(
carousel.querySelectorAll(":scope > [data-dang-carousel-slide]")
);
// A lone slide isn't a carousel; leave it as the plain block it already is.
if (slides.length < 2) return;

var active = -1;

// Tab strip: one tab per slide, labelled by the slide's feature title.
var tabs = document.createElement("div");
tabs.className = "dang-carousel-tabs";
tabs.setAttribute("role", "tablist");
var tabBtns = slides.map(function (slide, i) {
var titleEl = slide.querySelector(".dang-carousel-title");
var label = titleEl ? titleEl.textContent.trim() : "Example " + (i + 1);
var tab = document.createElement("button");
tab.className = "dang-carousel-tab";
tab.type = "button";
tab.textContent = label;
tab.setAttribute("role", "tab");
slide.setAttribute("role", "tabpanel");
tab.addEventListener("click", function () { show(i); });
tabs.appendChild(tab);
return tab;
});

// Footer: prev arrow, "n / total" counter, next arrow.
var foot = document.createElement("div");
foot.className = "dang-carousel-foot";
var prev = document.createElement("button");
prev.className = "dang-carousel-arrow";
prev.type = "button";
prev.setAttribute("aria-label", "Previous feature");
prev.innerHTML = "&#8249;"; // ‹
var count = document.createElement("span");
count.className = "dang-carousel-count";
var next = document.createElement("button");
next.className = "dang-carousel-arrow";
next.type = "button";
next.setAttribute("aria-label", "Next feature");
next.innerHTML = "&#8250;"; // ›
foot.appendChild(prev);
foot.appendChild(count);
foot.appendChild(next);
prev.addEventListener("click", function () { show(active - 1); });
next.addEventListener("click", function () { show(active + 1); });

carousel.insertBefore(tabs, carousel.firstChild);
carousel.appendChild(foot);
carousel.classList.add("is-enhanced");

function show(i) {
var n = slides.length;
i = ((i % n) + n) % n; // wrap around at both ends
if (i === active) return;
active = i;
slides.forEach(function (s, j) {
var on = j === i;
s.classList.toggle("is-active", on);
s.setAttribute("aria-hidden", on ? "false" : "true");
});
tabBtns.forEach(function (t, j) {
var on = j === i;
t.classList.toggle("is-active", on);
t.setAttribute("aria-selected", on ? "true" : "false");
t.tabIndex = on ? 0 : -1;
});
count.textContent = (i + 1) + " / " + n;
// The revealed slide embeds a \dang-literate editor whose textarea
// autosizes by measuring scrollHeight — which reads 0 while the slide is
// display:none. playground.js re-runs every editor's autosize on window
// resize, so nudging it here corrects the just-shown slide's height.
window.dispatchEvent(new Event("resize"));
}

// Left/right arrow keys page through — but not while editing a slide's
// code (the embedded editor needs the arrows to move the caret).
carousel.addEventListener("keydown", function (e) {
if (e.target && e.target.closest("textarea, input")) return;
if (e.key === "ArrowLeft") { e.preventDefault(); show(active - 1); }
else if (e.key === "ArrowRight") { e.preventDefault(); show(active + 1); }
});

show(0);
}

function init() {
var carousels = document.querySelectorAll("[data-dang-carousel]");
for (var i = 0; i < carousels.length; i++) enhance(carousels[i]);
}

if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", init);
} else {
init();
}
})();
15 changes: 14 additions & 1 deletion docs/js/playground.js
Original file line number Diff line number Diff line change
Expand Up @@ -1011,8 +1011,21 @@
for (var j = 0; j < repls.length; j++) enhanceRepl(repls[j]);
var lazies = document.querySelectorAll("[data-dang-repl-lazy]");
for (var k = 0; k < lazies.length; k++) enhanceLazyRepl(lazies[k]);
// Literate blocks replay as a chain (notebook semantics: one shared
// session, top to bottom). By default the whole page is one chain; a
// [data-dang-literate-chain] ancestor scopes an independent chain, so e.g.
// each carousel slide replays on its own fresh session instead of being
// folded into the page's chain.
var lits = document.querySelectorAll("[data-dang-literate]");
if (lits.length) enhanceLiterateChain(lits);
if (lits.length) {
var groups = new Map(); // chain root (element or document) -> blocks
for (var m = 0; m < lits.length; m++) {
var root = lits[m].closest("[data-dang-literate-chain]") || document;
if (!groups.has(root)) groups.set(root, []);
groups.get(root).push(lits[m]);
}
groups.forEach(function (blocks) { enhanceLiterateChain(blocks); });
}
}

// Capture any OAuth token handed back in the fragment before enhancing, so
Expand Down
71 changes: 71 additions & 0 deletions docs/lit/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,77 @@ functions are loaded directly from the schema.

\shell{go install github.com/vito/dang/v2/cmd/dang@latest}

\dang-carousel{
\dang-feature{Prototype objects}{{{
# `type` declares a type AND its constructor in one go
type User {
name: String!
greeting: String! { `Hi, I'm ${name}` }
}

User("Ada").greeting
}}}
}{
\dang-feature{Multi-field selection}{{{
type Repo {
name: String!
stars: Int! { 1000 }
}

# select many fields at once; against a GraphQL schema this is
# ONE query whose fields resolve in parallel
[Repo("dang"), Repo("booklit")].{{ name, stars }}.map { r => `${r.name} ★${r.stars}` }
}}}
}{
\dang-feature{Copy-on-write}{{{
type Counter {
n: Int!
bump: Counter! { n += 1; self }
}

let c = Counter(0)
# values are immutable — methods fork the receiver, so c never changes
[c.bump.bump.n, c.n]
}}}
}{
\dang-feature{Null tracking}{{{
# String may be null; String! cannot — the type system tracks the gap
let name: String = "Dang"

# inside the guard, name narrows from String to String!
if (name != null) { name.toUpper } else { "(none)" }
}}}
}{
\dang-feature{Optional parens}{{{
type Circle {
r: Float!
area: Float! { 3.14159 * r * r }
}

let c = Circle(2.0)
# r is stored, area is computed — but they're accessed identically
[c.r, c.area]
}}}
}{
\dang-feature{Everything is an expression}{{{
# case yields a value, like if/loop/try — assign it, return it, pass it
classify(n: Int!): String! {
case (n) {
0 => "zero"
else => if (n > 0) "positive" else "negative"
}
}

[classify(0), classify(7), classify(-3)]
}}}
}{
\dang-feature{Testing built in}{{{
# assert is a builtin — high-level testing, no framework
assert { [1, 2, 3].map { x => x * 2 } == [2, 4, 6] }
"tests pass"
}}}
}

\dang-playground{{{
# Edit me, then hit Run — this evaluates in your browser.
type Greeter {
Expand Down