diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md new file mode 100644 index 0000000000..f3fec1886b --- /dev/null +++ b/.claude/CLAUDE.md @@ -0,0 +1,275 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +Quarto is an open-source scientific and technical publishing system built on Pandoc. The CLI is written in TypeScript/Deno with Lua filters for document processing. + +### Versions + +Latest stable is the version at: . Latest prerelease is at: + +## Setup & Configuration + +### Initial Setup + +```bash +# Clone and configure (downloads Deno, dependencies, and sets up symlink) +./configure.sh # Linux/macOS +./configure.cmd # Windows + +# The configure script: +# - Downloads and installs Deno to package/dist/bin/tools/ +# - Downloads Deno standard library +# - Runs quarto-bld configure +# - Vendors TypeScript dependencies +# - Creates symlink to quarto in your PATH +``` + +After configuration, the development version can be run via: + +- `quarto` command (if symlink configured) +- `package/dist/bin/quarto` (Linux/macOS) or `package/dist/bin/quarto.cmd` (Windows) + +### Configuration Files + +- `configuration` - Version numbers for all binary and JavaScript dependencies (Deno, Pandoc, Dart Sass, etc.) +- `deno.jsonc` - Auto-generated Deno configuration (see dev-docs/update-deno_jsonc.md) +- `src/import_map.json` - Deno import mappings +- `src/dev_import_map.json` - Development import mappings + +## Building & Testing + +### Build Commands + +```bash +# Run the build script (TypeScript-based) +package/src/quarto-bld configure # Configure/bootstrap +package/src/quarto-bld prepare # Prepare distribution +``` + +### Building Schemas and Artifacts + +Use the `dev-call` command with `build-artifacts` argument: + +```bash +# Linux/macOS +package/dist/bin/quarto dev-call build-artifacts + +# Windows +package/dist/bin/quarto.cmd dev-call build-artifacts +``` + +This command regenerates: + +- JSON schemas in `src/resources/schema/json-schemas.json` +- Zod schemas in `src/resources/types/zod/schema-types.ts` +- TypeScript type definitions in `src/resources/types/schema-types.ts` +- Editor tooling files (VSCode IntelliSense, YAML intelligence) + +### Running Tests + +Tests live in `tests/` and require R, Python, and Julia. See `.claude/rules/testing/overview.md` for commands, test types, dependencies, and debugging tips. + +```bash +cd tests +./run-tests.sh smoke/render/render.test.ts # Linux/macOS +.\run-tests.ps1 smoke/render/render.test.ts # Windows +``` + +### Feature Format Matrix + +The feature format matrix in `dev-docs/feature-format-matrix/` documents and tests feature support across all output formats. + +- Test documents organized by feature in `qmd-files/` subdirectories +- Quality ratings in format metadata: `0` (broken/partial), `1` (good), `2` (excellent) +- Runs on CI via `.github/workflows/test-ff-matrix.yml` + +## Architecture + +### Entry Point & Commands + +- `src/quarto.ts` - Main CLI entry point +- `src/command/command.ts` - Command registration +- Commands are organized in `src/command/*/cmd.ts` files: + - `render/` - Core rendering functionality + - `preview/` - Live preview server + - `publish/` - Publishing to various platforms + - `create/` and `add/` - Project/extension scaffolding + - `tools/`, `install/`, `check/` - Utilities + +### Core Systems + +**Project System** (`src/project/`) + +- `types/` - Project type implementations (book, website, manuscript, etc.) +- Project types are registered via `project/types/register.ts` +- Each type defines metadata, rendering behavior, and output structure + +**Format System** (`src/format/`) + +- Format handlers for different output types (HTML, PDF, DOCX, reveal.js, etc.) +- `formats.ts` - Format registry and resolution +- `format-handlers.ts` - Common format handling logic + +**Filter System** (`src/resources/filters/`) + +- Lua filters process Pandoc AST during rendering +- `main.lua` - Entry point for filter chain +- Organized by function: `crossref/`, `layout/`, `quarto-pre/`, `quarto-post/`, `quarto-finalize/` +- Custom AST nodes in `customnodes/` +- Common utilities in `common/` + +**Execution Engines** (`src/execute/`) + +- Integration with Jupyter, Knitr, and Observable for code execution +- Engine-specific handling for Python, R, Julia, and JavaScript + +**Resources** (`src/resources/`) + +- Static assets, templates, and bundled libraries +- Format-specific resources (HTML, PDF, reveal.js templates) +- Extensions (Confluence, Docusaurus, etc.) +- Pandoc datadir customizations + +### Key Subsystems + +**Preview System** (`src/preview/`) + +- Development server with live reload +- Watches for file changes and re-renders + +**Publishing System** (`src/publish/`) + +- Platform-specific publishers (Netlify, GitHub Pages, Confluence, etc.) +- Account management and deployment logic + +**Extension System** (`src/extension/`) + +- Quarto extensions (filters, formats, shortcodes) +- Extension discovery, installation, and management + +### Package/Distribution + +- `package/` - Packaging and distribution scripts +- `package/src/quarto-bld` - Build orchestration script (TypeScript) +- Platform-specific packaging in `package/src/{linux,macos,windows}/` + +## Development Patterns + +### Debugging Flaky Tests + +Comprehensive methodology for debugging flaky tests documented in [dev-docs/debugging-flaky-tests.md](../dev-docs/debugging-flaky-tests.md). + +Key phases: +1. Reproduce locally (outside CI) +2. Binary search to isolate culprit test +3. Narrow down within test file +4. Understand state change +5. Identify root cause +6. Verify solution + +### TypeScript/Deno Conventions + +- Use Deno-native APIs (avoid Node.js APIs) +- Import maps resolve dependencies (see `src/import_map.json`) +- Cliffy library used for CLI parsing +- File paths in imports must include `.ts` extension + +### Lua Filter Development + +- Filters run during Pandoc processing pipeline +- Use `quarto` Lua module for Quarto-specific APIs +- Common utilities in `src/resources/filters/common/` +- Filters are chained together in `main.lua` +- Documentation: + +### Adding New Commands + +1. Create command file in `src/command//cmd.ts` +2. Export command using Cliffy's `Command` API +3. Register in `src/command/command.ts` + +### Adding New Project Types + +1. Create implementation in `src/project/types//` +2. Implement `ProjectType` interface +3. Register in `src/project/types/register.ts` + +### Adding New Formats + +1. Create format definition in `src/format//` +2. Implement format handler +3. Register in `src/format/imports.ts` + +### LaTeX Error Detection + +LaTeX error pattern maintenance is documented in [dev-docs/tinytex-pattern-maintenance.md](../dev-docs/tinytex-pattern-maintenance.md). + +- Patterns inspired by TinyTeX's comprehensive regex.json +- Automated daily verification workflow checks for TinyTeX pattern updates +- Pattern location: `src/command/render/latexmk/parse-error.ts` +- Verification workflow: `.github/workflows/verify-tinytex-patterns.yml` + +## Important Conventions + +- Main branch: `main` +- Version defined in `configuration` file in `QUARTO_VERSION` field +- Binary dependencies (Deno, Pandoc, etc.) versions in `configuration` +- Use `quarto-bld` for build operations, not direct Deno commands +- Lua filters use Pandoc's filter infrastructure +- TypeScript types for Lua APIs in `src/resources/lua-types/` + +### Changelog Conventions + +- Changelog files live in `news/changelog-{version}.md` (e.g., `changelog-1.9.md`) +- Check `configuration` file for current `QUARTO_VERSION` +- See `.claude/rules/changelog.md` for comprehensive conventions (section hierarchy, entry format, backports, regression fixes) + +## Key File Paths + +- Quarto binary: `package/dist/bin/quarto` (Linux/macOS) or `package/dist/bin/quarto.cmd` (Windows) +- Deno binary: `package/dist/bin/tools//deno` +- Distribution output: `package/dist/` +- Vendored dependencies: `src/vendor/` + +## Documentation + +- Documentation is at with a sitemap at +- Prerelease docs: for features in dev versions +- Dev documentation in `dev-docs/` includes: + - Checklists for releases and backports + - Dependency update procedures + - Internals guides + - Performance monitoring + +## Contributing + +See CONTRIBUTING.md for pull request guidelines. Significant changes require a signed contributor agreement (individual or corporate). + +## Maintaining Memory Files + +This project uses Claude Code memory files for AI-assisted development. When updating memory files: + +- **Add new feature area?** Create `.claude/rules//feature-name.md` with `paths:` frontmatter +- **Update existing feature?** Edit the relevant rule file +- **Deep dive doc needed?** Place it in `llm-docs/` and reference from rules + +**Memory file types:** + +| Location | When Loaded | Use For | +|----------|-------------|---------| +| `.claude/CLAUDE.md` | Always | Project overview, essential commands | +| `.claude/rules//` | When paths match | Feature-specific conventions | +| `llm-docs/` | When explicitly read | Architectural deep dives | + +**Personal overrides:** Create `CLAUDE.local.md` (gitignored) for personal preferences like preferred shell syntax or workflow customizations. This file is loaded alongside the project CLAUDE.md but won't be committed. + +For setup details, see [dev-docs/claude-code-setup.md](../dev-docs/claude-code-setup.md). + +## Additional Resources + +- **LLM Documentation**: `llm-docs/` contains AI-specific guidance for working with the codebase +- **Rules Files**: `.claude/rules/` contains conditional guidance for specific file patterns +- **DeepWiki**: for AI-indexed documentation diff --git a/.claude/rules/changelog.md b/.claude/rules/changelog.md new file mode 100644 index 0000000000..7413cf6991 --- /dev/null +++ b/.claude/rules/changelog.md @@ -0,0 +1,91 @@ +--- +paths: + - news/** +--- + +# Changelog Conventions + +## File Organization + +- One file per major.minor version: `news/changelog-{version}.md` (e.g., `changelog-1.9.md`) +- Check `configuration` file for current `QUARTO_VERSION` +- First line: `All changes included in {version}:` + +## Section Hierarchy (strict order) + +1. **`## Regression fixes`** - Always FIRST if present +2. **`## Dependencies`** - Bundled tool updates +3. **`## Formats`** - By output format (H3 subsections) +4. **`## Projects`** - By project type (H3 subsections) +5. **`## Publishing`** - By platform (H3 subsections) +6. **`## Lua API`** - Filter API changes +7. **`## Commands`** - CLI commands (H3 subsections) +8. **`## Extensions`** - Extension system changes +9. **`## Engines`** - Execution engines (H3 subsections) +10. **`## Other fixes and improvements`** - Always LAST + +### Format Subsections +Use H3 headings with backtick-wrapped names: +```markdown +### `html` +### `typst` +### `pdf` +``` + +## Entry Format + +```markdown +- ([#issue](url)): Description ending with period. +``` + +For external contributors (not core team): +```markdown +- ([#issue](url)): Description. (author: @username) +``` + +**Variations:** +- Pull requests: `([#13441](https://github.com/quarto-dev/quarto-cli/pull/13441))` +- External repos: `([rstudio/tinytex-releases#49](url))` +- No issue/PR (rare): Reference commit hash instead: `([commit](https://github.com/quarto-dev/quarto-cli/commit/abc123))` + +## Writing Entries + +**Language patterns:** +- **Fixes:** Start with "Fix" - describe what was broken + - "Fix `icon=false` not working with typst format." +- **Enhancements:** Start with "Add" or "Support" - describe what was added + - "Add support for `icon=false` in callouts." +- **Updates:** Start with "Update" + - "Update `pandoc` to 3.8.3" + +**Style:** +- Use backticks for code/options: `` `icon=false` `` +- Period at end of every description +- Author attribution `(author: @username)` for **external contributors only** - do NOT add for quarto-cli core team members + +## Regression Fixes + +**What qualifies:** Bugs introduced in recent versions (same major.minor) + +**Placement:** Always at TOP of the file, before Dependencies + +**Workflow:** If you initially place an entry elsewhere and later determine it's a regression, move it to the Regression fixes section + +## Backports + +When a fix is backported to a stable branch: +1. Entry exists in current version changelog (e.g., `changelog-1.9.md`) +2. **Also add entry to stable version changelog** (e.g., `changelog-1.8.md`) + +## What NOT to Put Here + +**Highlights** are NOT changelog entries. They are: +- Promotional content for release announcements +- Managed in quarto-web: `docs/prerelease/{version}/_highlights.qmd` +- Separate from technical changelog + +## Publication + +Changelogs are published to quarto.org via: +- GitHub release attaches `changelog.md` asset +- quarto-web fetches and displays at `/docs/download/changelog/{version}/` diff --git a/.claude/rules/dev-tools/dev-call-commands.md b/.claude/rules/dev-tools/dev-call-commands.md new file mode 100644 index 0000000000..af6dcb05f7 --- /dev/null +++ b/.claude/rules/dev-tools/dev-call-commands.md @@ -0,0 +1,96 @@ +--- +description: "Development commands reference for Quarto CLI development" +paths: + - "src/command/dev-call/**/*" +--- + +# Development Commands (dev-call) + +The `dev-call` command provides access to internal development tools. These commands are hidden from regular help output but essential for CLI development. + +## cli-info + +Generate JSON information about the Quarto CLI structure and commands. + +```bash +package/dist/bin/quarto dev-call cli-info # Linux/macOS +package/dist/bin/quarto.cmd dev-call cli-info # Windows +``` + +**Use cases**: Documentation generation, programmatic analysis of CLI structure, building CLI tooling. + +## validate-yaml + +Validate YAML files against Quarto's schema definitions. + +```bash +# Validate against built-in schema +package/dist/bin/quarto dev-call validate-yaml config.yml --schema document +package/dist/bin/quarto dev-call validate-yaml _quarto.yml --schema project/project + +# Validate against custom schema file +package/dist/bin/quarto dev-call validate-yaml custom.yml --schema my-schema.yml + +# Get JSON output +package/dist/bin/quarto dev-call validate-yaml config.yml --schema document --json +``` + +**Use cases**: Testing schema changes, debugging YAML configuration issues, validating custom schemas, CI/CD pipelines. + +**Schema names**: Reference definitions in `src/resources/schema/definitions.yml` (e.g., `document`, `project/project`, `format/html`). + +## build-artifacts + +Regenerate schemas, types, and editor tooling files. + +```bash +package/dist/bin/quarto dev-call build-artifacts # Linux/macOS +package/dist/bin/quarto.cmd dev-call build-artifacts # Windows +``` + +**Regenerates**: +- JSON schemas in `src/resources/schema/json-schemas.json` +- Zod schemas in `src/resources/types/zod/schema-types.ts` +- TypeScript type definitions in `src/resources/types/schema-types.ts` +- Editor tooling files (VSCode IntelliSense, YAML intelligence) + +## show-ast-trace + +Launch interactive viewer to visualize how a document transforms through the Lua filter pipeline. + +```bash +package/dist/bin/quarto dev-call show-ast-trace document.qmd +package/dist/bin/quarto dev-call show-ast-trace document.qmd --to html +``` + +**What it does**: + +1. Renders document with AST tracing enabled +2. Generates `-quarto-ast-trace.json` in cache +3. Launches interactive trace viewer in browser + +**Use cases**: Debugging Lua filter behavior, understanding AST transformations, investigating rendering issues, visualizing document structure changes. + +**Alternative**: Manually set `QUARTO_TRACE_FILTERS` environment variable during render. See `dev-docs/lua-filter-trace-viewer.qmd` for detailed guide. + +**Limitations**: + +- Doesn't work well with website/book projects (env var doesn't differentiate per file) +- Shows only Pandoc AST, not postprocessor or writer behavior + +## make-ast-diagram + +Create a static visual diagram of the Pandoc AST structure for a document. + +```bash +package/dist/bin/quarto dev-call make-ast-diagram document.qmd +package/dist/bin/quarto dev-call make-ast-diagram document.qmd --mode full +``` + +**Use cases**: Understanding document AST structure, debugging complex layouts, visualizing nested elements, teaching Pandoc AST concepts. + +**Note**: Simpler than full trace - provides static snapshot rather than filter chain progression. + +## Reference + +See `dev-docs/dev-call-commands.md` for comprehensive reference and workflows. diff --git a/.claude/rules/filters/lua-development.md b/.claude/rules/filters/lua-development.md new file mode 100644 index 0000000000..fdc9659e09 --- /dev/null +++ b/.claude/rules/filters/lua-development.md @@ -0,0 +1,243 @@ +--- +paths: + - "src/resources/filters/**/*.lua" +--- + +# Lua Filter Development Conventions + +Guidance for developing Lua filters in Quarto's filter system. + +## Module Loading + +### Use `import()` for Filter Files + +```lua +-- ✅ Correct - import from main.lua +import("./quarto-pre/shortcodes.lua") + +-- ❌ Wrong - require is for modules only +require("./quarto-pre/shortcodes") +``` + +### Use `require()` for Modules + +```lua +-- ✅ Correct - modules from modules/ +local patterns = require("modules/patterns") +local md = require("modules/md") + +-- ❌ Wrong - using import for modules +import("./modules/patterns.lua") +``` + +## Custom Node Patterns + +### Walking Custom Nodes + +Always use `_quarto.ast.walk()` to properly handle custom nodes: + +```lua +-- ✅ Correct - handles custom nodes +doc = _quarto.ast.walk(doc, { + Callout = function(callout) + -- Process callout + end +}) + +-- ❌ Wrong - misses custom nodes +pandoc.walk_block(div, filter) +``` + +### Checking Node Types + +```lua +-- Check if custom node of specific type +if is_custom_node(node, "Callout") then + -- Handle callout custom node +end + +-- Check if regular Pandoc node (NOT custom node) +if is_regular_node(node, "Div") then + -- Handle regular Div +end + +-- Check custom node by presence of is_custom_node flag +if node.is_custom_node then + -- It's some custom node type +end +``` + +### Slot Assignment + +Use the proxy pattern for slot modification: + +```lua +-- ✅ Correct - proxy pattern +local new_callout = callout:clone() +new_callout.content = modified_content +return new_callout + +-- ❌ Wrong - direct assignment may not work +callout.content = modified_content +return callout +``` + +## Format Detection + +Use `_quarto.format` for format checks: + +```lua +-- HTML output (includes HTML-based formats) +if _quarto.format.isHtmlOutput() then ... end + +-- LaTeX/PDF output +if _quarto.format.isLatexOutput() then ... end + +-- Typst output +if _quarto.format.isTypstOutput() then ... end + +-- Word/DOCX output +if _quarto.format.isDocxOutput() then ... end + +-- Reveal.js slides +if _quarto.format.isRevealJsOutput() then ... end + +-- Dashboard format +if _quarto.format.isDashboardOutput() then ... end +``` + +## Options and Parameters + +```lua +-- Read metadata option with default +local show_icon = option("callout-icon", true) + +-- Read execution parameter +local engine = param("execution-engine") + +-- Read option with nil fallback +local custom = option("my-option") +if custom ~= nil then + -- Option was set +end +``` + +## Logging + +Use logging functions from `common/log.lua`: + +```lua +-- Debug info (verbose) +info("Processing element: " .. el.t) + +-- Warnings (appear as INFO on TypeScript side) +warn("Deprecated feature used") + +-- Errors +error("Invalid configuration") + +-- Conditional debug output +if quarto.log.debug then + quarto.utils.dump(node) +end +``` + +## Filter Return Values + +```lua +function my_filter() + return { + Div = function(div) + -- Return nil to continue (no change) + if not should_process(div) then + return nil + end + + -- Return new element to replace + return pandoc.Div(modified_content) + + -- Return empty list to remove element + -- return {} + end + } +end +``` + +## Common Utilities + +### String Operations + +```lua +-- String matching +if string.match(text, "pattern") then ... end + +-- String substitution +local result = string.gsub(text, "old", "new") + +-- Check class presence +if div.classes:includes("callout") then ... end + +-- Check attribute +local value = div.attributes["data-foo"] +``` + +### Pandoc Helpers + +```lua +-- Create elements +local div = pandoc.Div(content, pandoc.Attr(id, classes, attributes)) +local span = pandoc.Span(inlines) +local para = pandoc.Para(inlines) + +-- Raw output +local raw = pandoc.RawBlock("html", "
...
") +local raw = pandoc.RawInline("latex", "\\textbf{}") + +-- Stringify content +local text = pandoc.utils.stringify(inlines) +``` + +## Debugging + +```lua +-- Pretty-print any object +quarto.utils.dump(node) + +-- Type checking +print("Type: " .. type(obj)) +print("Pandoc type: " .. (obj.t or "none")) + +-- Trace execution +warn("Reached checkpoint: " .. checkpoint_name) +``` + +## Filter Chain Integration + +When adding filters to `main.lua`: + +```lua +-- Each filter gets a name and filter function +{ name = "pre-my-feature", filter = my_feature() } + +-- Filter order matters - check dependencies +-- Filters in same stage run in order defined +``` + +## API Reference + +Consult `src/resources/lua-types/` for available methods, properties, and function signatures: + +- `lua-types/pandoc/` - Pandoc Lua API (blocks, inlines, List, utils, etc.) +- `lua-types/quarto/` - Quarto Lua API (format detection, custom nodes, etc.) + +These type definition files document the complete API surface. + +## Key Conventions Summary + +1. **Use `import()` for filters** - `require()` for modules only +2. **Use `_quarto.ast.walk()`** - Not `pandoc.walk_*` for custom nodes +3. **Check node types carefully** - `is_custom_node()` vs `is_regular_node()` +4. **Use proxy pattern** - For modifying custom node slots +5. **Use `_quarto.format`** - For format detection +6. **Return `nil` to continue** - Return value replaces element +7. **`warn()` = INFO level** - On TypeScript side diff --git a/.claude/rules/filters/overview.md b/.claude/rules/filters/overview.md new file mode 100644 index 0000000000..fc1f410431 --- /dev/null +++ b/.claude/rules/filters/overview.md @@ -0,0 +1,83 @@ +--- +paths: + - "src/resources/filters/**" +--- + +# Lua Filter System + +Quarto's Lua filter system is a multi-stage document transformation pipeline that processes Pandoc AST through ~212 Lua files. + +**For coding conventions:** See `.claude/rules/filters/lua-development.md` + +## Directory Structure + +``` +filters/ +├── main.lua # Entry point - filter chain definition +├── mainstateinit.lua # Global state initialization +├── ast/ # Custom AST infrastructure +├── common/ # Shared utilities (~35 files) +├── modules/ # Reusable modules (require()) +├── customnodes/ # Custom node implementations +├── quarto-init/ # Initialization stage +├── normalize/ # Normalization stage +├── quarto-pre/ # Pre-processing (shortcodes, tables, etc.) +├── crossref/ # Cross-reference system +├── layout/ # Layout processing +├── quarto-post/ # Post-processing (format-specific) +└── quarto-finalize/ # Final cleanup +``` + +## Filter Execution Pipeline + +``` +1. INIT (quarto-init/) + ↓ +2. NORMALIZE (normalize/) + ↓ +3. PRE (quarto-pre/) - shortcodes, tables, code annotations + ↓ +4. CROSSREF (crossref/) - cross-references + ↓ +5. LAYOUT (layout/) - panels, columns + ↓ +6. POST (quarto-post/) - format-specific rendering + ↓ +7. FINALIZE (quarto-finalize/) - cleanup, dependencies +``` + +User filters run between stages via entry points (`pre-ast`, `post-ast`, `pre-quarto`, etc.). + +## Key Files + +| File | Purpose | +|------|---------| +| `main.lua` | Filter chain definition (~725 lines) | +| `mainstateinit.lua` | Global state initialization | +| `ast/customnodes.lua` | Custom node system | +| `common/log.lua` | Logging utilities | +| `common/debug.lua` | Debug utilities (`dump`, `tdump`) | +| `common/format.lua` | Format detection | +| `common/options.lua` | Metadata option reading | + +## Debugging + +**Filter tracing (recommended):** +```bash +# Linux/macOS +package/dist/bin/quarto dev-call show-ast-trace document.qmd + +# Windows +package/dist/bin/quarto.cmd dev-call show-ast-trace document.qmd +``` + +**AST diagram:** +```bash +quarto dev-call make-ast-diagram document.qmd +``` + +## Related Documentation + +- **Coding conventions**: `.claude/rules/filters/lua-development.md` +- **Lua API**: +- **Filter tracing**: `dev-docs/lua-filter-trace-viewer.qmd` diff --git a/.claude/rules/formats/format-handlers.md b/.claude/rules/formats/format-handlers.md new file mode 100644 index 0000000000..f6a855bf5e --- /dev/null +++ b/.claude/rules/formats/format-handlers.md @@ -0,0 +1,220 @@ +--- +paths: + - "src/format/**/*" +--- + +# Format System + +Guidance for working with the Quarto format system. + +## Architecture Overview + +``` +Format Resolution Flow: +┌──────────────────────────────────────────────────┐ +│ defaultWriterFormat(formatString) [formats.ts] │ +│ │ +│ 1. Check registered handlers (writerFormatHandlers) +│ 2. Fall back to built-in switch statement │ +│ 3. Apply format variants (mergeFormatVariant) │ +└──────────────────────────────────────────────────┘ +``` + +## Key Files + +| File | Purpose | +|------|---------| +| `formats.ts` | Central `defaultWriterFormat()` resolver | +| `format-handlers.ts` | Handler registration API | +| `formats-shared.ts` | Factory functions (`createFormat`, `createHtmlFormat`) | +| `imports.ts` | Side-effect imports for format registration | + +## Format Handler Pattern + +Modern formats use registration to avoid circular dependencies: + +```typescript +// src/format//format-.ts +import { registerWriterFormatHandler } from "../format-handlers.ts"; + +function myFormat(): Format { + return createFormat("My Format", "html", { + pandoc: { to: "html" }, + render: { /* ... */ }, + // ... + }); +} + +// Register at module scope (runs on import) +registerWriterFormatHandler((format) => { + if (format === "myformat") { + return { + format: myFormat(), + pandocTo: "html", // Optional: override Pandoc writer + }; + } +}); +``` + +Then add to `imports.ts`: +```typescript +import "./myformat/format-myformat.ts"; +``` + +## Format Structure + +```typescript +interface Format { + identifier: FormatIdentifier; // Display name, base/target format + render: FormatRender; // Rendering options (keep-tex, etc.) + execute: FormatExecute; // Execution (fig-width, echo, cache) + pandoc: FormatPandoc; // Pandoc args (to, from, template) + language: FormatLanguage; // Localized strings + metadata: Metadata; // Document metadata + + // Optional hooks + resolveFormat?: (format: Format) => void; + formatExtras?: (...) => Promise; + extensions?: { book?: BookExtension }; +} +``` + +## Factory Functions + +**Base format:** +```typescript +import { createFormat } from "../formats-shared.ts"; + +const format = createFormat( + "My Format", // Display name + "html", // File extension + baseFormat1, // Formats to merge (in order) + baseFormat2, +); +``` + +**HTML-based:** +```typescript +import { createHtmlFormat, htmlFormat } from "../formats-shared.ts"; + +const format = createHtmlFormat("My HTML", 7, 5); // figwidth, figheight +``` + +**Extend existing:** +```typescript +import { mergeConfigs } from "../../core/config.ts"; + +const format = mergeConfigs( + htmlFormat(7, 5), // Base format + { + render: { echo: false }, + execute: { warning: false }, + }, +); +``` + +## FormatExtras Hook + +The `formatExtras()` hook adds format-specific processing: + +```typescript +formatExtras: async ( + input: string, + markdown: string, + flags: RenderFlags, + format: Format, + libDir: string, + services: RenderServices, + offset?: string, + project?: ProjectContext, + quiet?: boolean, +) => { + return { + pandoc: { /* additional pandoc args */ }, + html: { + dependencies: [/* scripts, stylesheets */], + sass: [/* Sass bundles */], + }, + postprocessors: [/* DOM manipulation functions */], + // ... + }; +}, +``` + +## Adding a New Format + +**Step 1:** Create format file + +```typescript +// src/format/myformat/format-myformat.ts +import { createFormat } from "../formats-shared.ts"; +import { registerWriterFormatHandler } from "../format-handlers.ts"; +import { mergeConfigs } from "../../core/config.ts"; + +function myFormat(): Format { + return mergeConfigs( + createFormat("My Format", "html"), + { + pandoc: { + to: "html", + // format-specific pandoc options + }, + execute: { + echo: false, // example: hide code by default + }, + }, + ); +} + +registerWriterFormatHandler((format) => { + if (format === "myformat") { + return { + format: myFormat(), + pandocTo: "html", + }; + } +}); +``` + +**Step 2:** Add to imports + +```typescript +// src/format/imports.ts +import "./myformat/format-myformat.ts"; +``` + +**Step 3:** Test + +```yaml +# test.qmd +--- +format: myformat +--- +Test content +``` + +## Directory Structure + +``` +src/format/ +├── format-handlers.ts # Registration API +├── formats.ts # Central resolver +├── formats-shared.ts # Factory functions +├── imports.ts # Initialization +├── html/ # HTML + variants +├── reveal/ # Reveal.js +├── dashboard/ # Dashboard +├── pdf/ # PDF, Beamer +├── typst/ # Typst +├── docx/ # DOCX +└── ... +``` + +## Key Concepts + +1. **Registration order** - Handlers checked before hardcoded formats +2. **Format composition** - Use `mergeConfigs()` to layer formats +3. **Pandoc writer override** - Handler can map format → different pandoc writer +4. **Lazy initialization** - Side-effect imports prevent circular deps +5. **formatExtras** - Pre-Pandoc setup (dependencies, sass) +6. **postprocessors** - Post-Pandoc file manipulation diff --git a/.claude/rules/llm-docs-maintenance.md b/.claude/rules/llm-docs-maintenance.md new file mode 100644 index 0000000000..16d5b97dde --- /dev/null +++ b/.claude/rules/llm-docs-maintenance.md @@ -0,0 +1,57 @@ +--- +paths: + - "llm-docs/**" +--- + +# LLM Documentation Maintenance + +The `llm-docs/` directory contains documentation written for LLM assistants working on the Quarto codebase. These docs capture architectural understanding that would otherwise require extensive codebase exploration. + +## Staleness Check + +Each document has YAML frontmatter with analysis metadata: + +```yaml +--- +main_commit: abc1234 # merge-base with main (stable reference) +analyzed_date: 2025-01-22 +key_files: + - path/to/file1.ts + - path/to/file2.lua +--- +``` + +**Why merge-base?** Branch commits can be rebased or disappear. The merge-base with main is stable and represents the baseline from main that was analyzed. + +**Before relying on a document**, check if key files have changed: + +```bash +git log --oneline ..main -- +``` + +If there are significant changes, re-explore the codebase and update the document. + +## Updating Documents + +After re-analyzing, update the frontmatter: + +```bash +# Get merge-base with main (use upstream/main if that's the main remote) +git merge-base HEAD main | cut -c1-9 +``` + +Then update `main_commit`, `analyzed_date`, and verify `key_files` list is complete. + +**Date verification:** Before writing dates, check today's date from the system environment (shown at conversation start). This avoids year typos like writing 2025 when it's 2026. + +## Document Purpose + +These docs are: +- Architectural overviews for AI assistants +- File location maps for common tasks +- Pattern documentation for consistency + +They are NOT: +- User documentation (that's at quarto.org) +- Code comments (those live in source files) +- Issue-specific notes (those go in PR descriptions) diff --git a/.claude/rules/rendering/render-pipeline.md b/.claude/rules/rendering/render-pipeline.md new file mode 100644 index 0000000000..8f30f11c09 --- /dev/null +++ b/.claude/rules/rendering/render-pipeline.md @@ -0,0 +1,179 @@ +--- +paths: + - "src/command/render/**/*" + - "src/render/**/*" +--- + +# Render Pipeline + +Guidance for working with the Quarto render system. + +## Render Flow Overview + +``` +CLI (cmd.ts) + ↓ +render() (render-shared.ts) + ↓ +┌─────────────────────────────────────────────────┐ +│ Decision: What type of render? │ +│ │ +│ 1. Project file exists + file in project │ +│ → renderProject() │ +│ │ +│ 2. --output-dir without project file │ +│ → Synthetic project → renderProject() │ +│ │ +│ 3. Single file │ +│ → singleFileProjectContext() → renderFiles() │ +└─────────────────────────────────────────────────┘ + ↓ +Per-file pipeline: Execute → Pandoc → Postprocess +``` + +## Key Files and Responsibilities + +| File | Purpose | +|------|---------| +| `cmd.ts` | CLI command handler, argument parsing | +| `render-shared.ts` | Top-level orchestration, render type decision | +| `render-contexts.ts` | Format resolution, metadata merging | +| `render-files.ts` | Per-file render pipeline, freezer logic | +| `project.ts` | Project-level orchestration, output management | +| `render.ts` | Core `renderPandoc()`, postprocessors | +| `types.ts` | Type definitions (`RenderOptions`, `RenderContext`) | + +## Synthetic Project Context Pattern + +**Critical non-obvious behavior** for `--output-dir` without a project file: + +When a user runs: +```bash +quarto render file.qmd --output-dir output/ +``` + +And no `_quarto.yml` exists, Quarto: + +1. Creates a temporary `.quarto` directory +2. Uses full `renderProject()` path (NOT `singleFileProjectContext()`) +3. Sets `forceClean` flag to signal cleanup needed +4. After rendering: closes file handles, removes `.quarto` + +**Key locations:** +- Creation: `render-shared.ts:52-60` +- Cleanup: `project.ts:889-907` +- Flag: `types.ts:38` + +**Why this matters:** +- Prevents `.quarto` debris in non-project directories (#9745) +- Windows file locking requires careful cleanup ordering (#13625) + +```typescript +// render-shared.ts - Decision logic +let context = await projectContext(path, nbContext, options); + +if (!context && options.flags?.outputDir) { + // Create synthetic project for --output-dir + context = await projectContextForDirectory(path, nbContext, options); + options.forceClean = options.flags.clean !== false; +} + +if (context?.config && isProjectInputFile(path, context)) { + return renderProject(context, options, [path]); +} else { + context = await singleFileProjectContext(path, nbContext, options); + return renderFiles([{ path }], options, ...); +} +``` + +## Format Resolution + +Metadata merges in this order (later overrides earlier): + +1. **Project metadata** (`_quarto.yml`) +2. **Directory metadata** (`_metadata.yml`) +3. **File metadata** (YAML frontmatter) +4. **Extension formats** (custom extensions) +5. **Default writer format** (built-in defaults) + +Implemented in `render-contexts.ts`. + +## Execute → Pandoc → Postprocess + +**Phase 1: Execute** (`render-files.ts`) +- Check freezer for cached results +- Run execution engine (Jupyter, Knitr, etc.) +- Handle language cells +- Produce markdown + metadata + +**Phase 2: Pandoc** (`render.ts`) +- Merge includes from execute result +- Run `runPandoc()` with filters +- Generate output file + +**Phase 3: Postprocess** (`render.ts`) +- Engine postprocess +- HTML postprocessors (DOM manipulation) +- Generic postprocessors +- Recipe completion (LaTeX → PDF) +- Self-contained output +- Cleanup + +## Cleanup Patterns + +**Normal project:** +```typescript +// Controlled by --clean flag +if (options.clean && renderAll) { + cleanOutputDir(outputDir); +} +``` + +**Synthetic project (forceClean):** +```typescript +// project.ts - Must close handles before removing files +context.cleanup(); // Close file handles +safeRemoveSync(join(projDir, kQuartoScratch)); // Remove .quarto +``` + +**Critical for Windows:** Close handles before removing files to avoid "file in use" errors. + +## RenderContext Structure + +```typescript +interface RenderContext { + target: ExecutionTarget; // Input file metadata + options: RenderOptions; // Flags, services, pandocArgs + engine: ExecutionEngineInstance; // Jupyter, Knitr, etc. + format: Format; // Resolved format config + libDir: string; // Library directory path + project: ProjectContext; // Project context (may be synthetic) + active: boolean; // Is this format being rendered? +} +``` + +## Important Flags + +| Flag | Purpose | +|------|---------| +| `--output-dir` | Output directory (triggers synthetic project if no project file) | +| `--to` | Target format(s) | +| `--execute` / `--no-execute` | Control code execution | +| `--clean` / `--no-clean` | Control output cleanup | +| `forceClean` (internal) | Signals synthetic project cleanup | + +## Development Considerations + +When modifying render code: + +1. **Consider both paths** - Project render vs single-file render +2. **Synthetic cleanup** - `--output-dir` without project needs cleanup +3. **Windows file locking** - Close handles before removing files +4. **Format resolution** - Multi-level merge is complex +5. **Project types** - Book/website/manuscript customize behavior + +## Testing + +- Smoke tests: `tests/smoke/render/` +- Output-dir tests: `tests/smoke/render/render-output-dir.test.ts` +- Document tests: `tests/docs/smoke-all/` with `_quarto` metadata diff --git a/.claude/rules/schemas/zod-usage.md b/.claude/rules/schemas/zod-usage.md new file mode 100644 index 0000000000..1c3f3e61bb --- /dev/null +++ b/.claude/rules/schemas/zod-usage.md @@ -0,0 +1,67 @@ +--- +paths: + - "src/resources/schema/**/*" + - "src/resources/types/**/*" + - "src/core/brand/**/*" + - "src/project/**/*" +--- + +# Zod Schema Usage Patterns + +Guidance on when to use Zod validation vs type assertions in the Quarto codebase. + +## Overview + +Zod schemas are generated from YAML schema definitions but have **specific use cases**. Understanding when to use each approach is critical. + +## When Zod IS Used (Entry Points for External Data) + +Use `Zod.*.parse()` for runtime validation when: + +- Loading brand data from `_brand.yml` files ([brand.ts:65](src/core/brand/brand.ts)) +- Processing extension metadata ([project-context.ts:128](src/project/project-context.ts)) +- Parsing metadata from files at boundaries ([project-shared.ts:606](src/project/project-shared.ts)) +- Validating complex navigation structures ([book-config.ts:262](src/project/types/book/book-config.ts)) + +```typescript +// Example: Validating external data at entry points +const brand = Zod.BrandSingle.parse(externalBrandData); +const navItem = Zod.NavigationItemObject.parse(item); +``` + +## When Type Assertions ARE Used (Internal Processing) + +Most code works with `project.config` which has **already been validated** when loaded via `readAndValidateYamlFromFile()` ([project-context.ts:463](src/project/project-context.ts)). In these cases, use type assertions with `Metadata`: + +```typescript +// Standard pattern for already-validated config data +const siteMeta = project.config?.[kWebsite] as Metadata; +if (siteMeta) { + const configValue = siteMeta[kConfigKey]; + if (typeof configValue === "object") { + const configMeta = configValue as Metadata; + const property = configMeta[kPropertyName] as string; + // ... use property + } +} +``` + +## Key Principle + +YAML schema validation happens at config load time, so downstream code doesn't need redundant Zod validation. Reserve Zod for true entry points where external/untrusted data enters the system. + +## File Locations + +- YAML schema definitions: `src/resources/schema/definitions.yml` +- Generated Zod schemas: `src/resources/types/zod/schema-types.ts` +- Generated TypeScript types: `src/resources/types/schema-types.ts` +- JSON schemas: `src/resources/schema/json-schemas.json` + +## Regenerating Schemas + +After modifying YAML schema definitions: + +```bash +package/dist/bin/quarto dev-call build-artifacts # Linux/macOS +package/dist/bin/quarto.cmd dev-call build-artifacts # Windows +``` diff --git a/.claude/rules/testing/overview.md b/.claude/rules/testing/overview.md new file mode 100644 index 0000000000..bec82f921b --- /dev/null +++ b/.claude/rules/testing/overview.md @@ -0,0 +1,71 @@ +--- +paths: + - "tests/**" +--- + +# Test Infrastructure + +Quarto's test suite lives in `tests/`. For comprehensive documentation, see `tests/README.md`. + +## Running Tests + +```bash +cd tests + +# Linux/macOS +./run-tests.sh # All tests +./run-tests.sh smoke/render/render.test.ts # Specific test +./run-tests.sh docs/smoke-all/path/test.qmd # Smoke-all document + +# Windows (PowerShell 7+) +.\run-tests.ps1 +.\run-tests.ps1 smoke/render/render.test.ts +``` + +**Skip dependency configuration:** +```bash +QUARTO_TESTS_NO_CONFIG="true" ./run-tests.sh test.ts # Linux/macOS +$env:QUARTO_TESTS_NO_CONFIG=$true; .\run-tests.ps1 # Windows +``` + +## Test Types + +| Type | Location | File Pattern | Details | +|------|----------|--------------|---------| +| Unit | `tests/unit/` | `*.test.ts` | `.claude/rules/testing/typescript-tests.md` | +| Smoke | `tests/smoke/` | `*.test.ts` | `.claude/rules/testing/typescript-tests.md` | +| Smoke-all | `tests/docs/smoke-all/` | `*.qmd` | `.claude/rules/testing/smoke-all-tests.md` | +| Playwright | `tests/integration/playwright/` | `*.spec.ts` | `.claude/rules/testing/playwright-tests.md` | + +## Dependencies + +Tests require R, Python, and Julia. Run configuration script to set up: + +```bash +# Linux/macOS +./configure-test-env.sh + +# Windows +.\configure-test-env.ps1 +``` + +Managed via: +- **R**: renv (`renv.lock`) +- **Python**: uv (`pyproject.toml`, `uv.lock`) +- **Julia**: Pkg.jl (`Project.toml`, `Manifest.toml`) + +## Core Files + +| File | Purpose | +|------|---------| +| `test.ts` | Test infrastructure (`testQuartoCmd`, `unitTest`) | +| `verify.ts` | Verification functions | +| `utils.ts` | Path utilities (`docs()`, `outputForInput()`) | +| `README.md` | Comprehensive documentation | + +## Debugging + +1. Run single test to isolate failures +2. Check render output - tests capture stdout/stderr +3. VSCode debugging via `.vscode/launch.json` +4. Flaky test methodology: [dev-docs/debugging-flaky-tests.md](../../../dev-docs/debugging-flaky-tests.md) diff --git a/.claude/rules/testing/playwright-tests.md b/.claude/rules/testing/playwright-tests.md new file mode 100644 index 0000000000..229087cc62 --- /dev/null +++ b/.claude/rules/testing/playwright-tests.md @@ -0,0 +1,97 @@ +--- +paths: + - "tests/integration/playwright/**/*.spec.ts" + - "tests/integration/playwright/**/*.ts" +--- + +# Playwright Tests + +Browser-based tests for interactive features. Tests live in `tests/integration/playwright/tests/`. + +## Local Development Workflow + +**Don't run `playwright-tests.test.ts` locally** - it renders ALL test documents which is slow. + +Instead, follow this pattern: + +```bash +# 1. Create/edit test documents +# Location: tests/docs/playwright// + +# 2. Render the documents you're working on +./package/dist/bin/quarto render tests/docs/playwright/html/tabsets/test.qmd +# on windows +./package/dist/bin/quarto.cmd render tests/docs/playwright/html/tabsets/test.qmd + +# 3. Run playwright from the playwright directory +cd tests/integration/playwright +npx playwright test html-tabsets.spec.ts # Single test +npx playwright test --grep "tabset" # Filter by name +``` + +## CI Execution + +On CI, tests run via the wrapper which handles rendering: + +```bash +./run-tests.sh integration/playwright-tests.test.ts +``` + +The wrapper (`playwright-tests.test.ts`): + +1. Renders all `.qmd` in `docs/playwright/` +2. Installs multiplex server dependencies +3. Runs `npx playwright test` +4. Cleans up rendered output + +## Test Structure + +Tests use `@playwright/test` framework: + +```typescript +import { test, expect } from "@playwright/test"; + +test("Feature description", async ({ page }) => { + await page.goto("/html/feature/test.html"); + + const element = page.getByRole("tab", { name: "Tab 1" }); + await expect(element).toHaveClass(/active/); + await element.click(); + await expect(page.locator("div.content")).toBeVisible(); +}); +``` + +## Configuration + +- **Config file:** `playwright.config.ts` +- **Base URL:** `http://127.0.0.1:8080` +- **Test documents:** `tests/docs/playwright//` +- **Test specs:** `tests/integration/playwright/tests/*.spec.ts` + +## Web Server + +Playwright starts a Python HTTP server automatically when running tests: + +```bash +uv run python -m http.server 8080 +# Serves from tests/docs/playwright/ +``` + +## Utilities + +From `src/utils.ts`: + +| Function | Purpose | +| ---------------------------------- | ------------------------- | +| `getUrl(path)` | Build full URL from path | +| `ojsVal(page, name)` | Get OJS runtime value | +| `ojsRuns(page)` | Wait for OJS to finish | +| `checkColor(element, prop, color)` | Verify CSS color | +| `useDarkLightMode(mode)` | Set color scheme for test | + +## Environment Variables + +| Variable | Effect | +| ------------------------------------------ | ---------------------------------- | +| `QUARTO_PLAYWRIGHT_TESTS_SKIP_RENDER` | Skip rendering (use existing HTML) | +| `QUARTO_PLAYWRIGHT_TESTS_SKIP_CLEANOUTPUT` | Keep rendered files after test | diff --git a/.claude/rules/testing/smoke-all-tests.md b/.claude/rules/testing/smoke-all-tests.md new file mode 100644 index 0000000000..a06be9e17c --- /dev/null +++ b/.claude/rules/testing/smoke-all-tests.md @@ -0,0 +1,137 @@ +--- +paths: + - "tests/docs/smoke-all/**/*.qmd" + - "tests/docs/smoke-all/**/*.md" + - "tests/docs/smoke-all/**/*.ipynb" + - "tests/smoke/smoke-all.test.ts" + - "tests/verify.ts" +--- + +# Smoke-All Test Format + +Document-based tests using YAML metadata for verification. Tests live in `tests/docs/smoke-all/`. + +## Running Tests + +```bash +# Linux/macOS +./run-tests.sh docs/smoke-all/path/to/test.qmd +./run-tests.sh docs/smoke-all/2023/**/*.qmd # Glob pattern + +# Windows +.\run-tests.ps1 docs/smoke-all/path/to/test.qmd +``` + +## Test Structure + +Tests are defined in `_quarto.tests` YAML metadata: + +```yaml +--- +title: My Test +_quarto: + tests: + html: # Format to test + ensureHtmlElements: # Verification function + - ['div.callout'] # Must exist + - ['div.error', false] # Must NOT exist +--- +``` + +## Available Verification Functions + +See `tests/smoke/smoke-all.test.ts` for the `verifyMap` that defines available functions. + +Common patterns: + +```yaml +_quarto: + tests: + html: + ensureHtmlElements: + - ['div.callout'] # Element must exist + - ['div.error', false] # Element must NOT exist + noErrorsOrWarnings: [] # Clean render (default) + noErrors: [] # Allow warnings + + typst: + ensureTypstFileRegexMatches: # Requires keep-typ: true + - ['#callout\('] + + pdf: + ensureLatexFileRegexMatches: # Requires keep-tex: true + - ['\\begin\{figure\}'] +``` + +### Message Verification + +Lua `warn()` appears as `level: INFO` on TypeScript side: + +```yaml +_quarto: + tests: + html: + printsMessage: + level: INFO + regex: 'WARNING(.*)text' + negate: false # Set true to verify absence +``` + +### Snapshot Testing + +```yaml +_quarto: + tests: + html: + ensureSnapshotMatches: [] +``` + +Save snapshot as `output.html.snapshot` alongside expected output. + +## Execution Control + +```yaml +_quarto: + tests: + run: + skip: true # Skip unconditionally + skip: "Reason for skipping" # Skip with message + ci: false # Skip on CI only + os: darwin # Run only on macOS + os: [windows, darwin] # Run on Windows or macOS + not_os: linux # Don't run on Linux +``` + +Valid OS values: `linux`, `darwin`, `windows` + +## Pattern Specificity + +Avoid patterns that match template boilerplate instead of document content: + +- Bad: `'#figure\('` - matches any figure including template definitions +- Good: `'#figure\(\[(\r\n?|\n)#block\['` - matches specific document structure + +**Line breaks:** `(\r\n?|\n)` for exact line breaks; `\s*` or `\s+` for flexible whitespace. + +**Examples:** `tests/docs/smoke-all/typst/`, `tests/docs/smoke-all/crossrefs/` + +## YAML String Escaping for Regex + +**Details:** `llm-docs/testing-patterns.md` → "YAML String Escaping for Regex" + +**Quick rule:** In YAML single quotes, use single backslash: `'\('` matches `\(`. Double-escaping `'\\('` is wrong. + +## File Organization + +**Issue-based:** `tests/docs/smoke-all/YYYY/MM/DD/.qmd` +**Feature-based:** `tests/docs/smoke-all//` + +## Creating New Tests + +```bash +# Linux/macOS +./new-smoke-all-test.sh 13589 + +# Windows +.\new-smoke-all-test.ps1 13589 +``` diff --git a/.claude/rules/testing/test-anti-patterns.md b/.claude/rules/testing/test-anti-patterns.md new file mode 100644 index 0000000000..ab18275a71 --- /dev/null +++ b/.claude/rules/testing/test-anti-patterns.md @@ -0,0 +1,19 @@ +--- +paths: + - "tests/**/*.ts" + - "tests/**/*.test.ts" +--- + +# Test Anti-Patterns + +## Don't: Modify Environment Variables + +`Deno.env.set()` modifies process-global state. Deno runs test files in parallel by default, so other tests can see modified values. + +**Details:** `llm-docs/testing-patterns.md` → "Environment Variable Testing Pitfalls" + +## Don't: Create Language Environments in Test Subdirectories + +Never create `Project.toml`, `.venv/`, or `renv.lock` in test fixture directories. + +**Details:** `llm-docs/testing-patterns.md` → "Shared Test Environments" diff --git a/.claude/rules/testing/typescript-tests.md b/.claude/rules/testing/typescript-tests.md new file mode 100644 index 0000000000..42870a00e7 --- /dev/null +++ b/.claude/rules/testing/typescript-tests.md @@ -0,0 +1,94 @@ +--- +paths: + - "tests/smoke/**/*.test.ts" + - "tests/unit/**/*.test.ts" +--- + +# TypeScript Tests + +TypeScript-based tests using Deno. Smoke tests render documents; unit tests verify isolated functionality. + +## Running Tests + +```bash +# Linux/macOS +./run-tests.sh smoke/render/render.test.ts # Specific smoke test +./run-tests.sh unit/path.test.ts # Specific unit test +./run-tests.sh smoke/extensions/ # Directory + +# Windows +.\run-tests.ps1 smoke/render/render.test.ts +``` + +## Core Infrastructure + +| File | Purpose | +|------|---------| +| `tests/test.ts` | `testQuartoCmd()`, `testRender()`, `unitTest()` | +| `tests/verify.ts` | Verification functions (`noErrors`, `fileExists`, etc.) | +| `tests/utils.ts` | `docs()`, `outputForInput()`, path utilities | + +## Smoke Tests (`tests/smoke/`) + +Render documents and verify output: + +```typescript +import { testRender } from "./render.ts"; +import { noErrorsOrWarnings } from "../../verify.ts"; +import { docs } from "../../utils.ts"; + +testRender( + "test.qmd", + "html", + false, // Keep output? + [noErrorsOrWarnings], + { cwd: () => docs("my-feature/") } +); +``` + +With setup/teardown: +```typescript +testRender("test.qmd", "html", false, [noErrorsOrWarnings], { + cwd: () => inputDir, + setup: async () => { /* Create temp files */ }, + teardown: async () => { /* Cleanup */ }, +}); +``` + +## Unit Tests (`tests/unit/`) + +Test isolated functionality: + +```typescript +import { unitTest } from "../test.ts"; +import { assert, assertEquals } from "testing/asserts"; + +// deno-lint-ignore require-await +unitTest("feature - description", async () => { + const result = myFunction(input); + assertEquals(result, expected); +}); +``` + +Assertions from `testing/asserts`: `assert`, `assertEquals`, `assertThrows`, `assertRejects` + +## Common Patterns + +**Temp files:** +```typescript +const workingDir = Deno.makeTempDirSync({ prefix: "quarto-test" }); +// Use workingDir, clean up in teardown +``` + +**Fixtures:** +```typescript +const fixtureDir = docs("my-fixture"); // → tests/docs/my-fixture/ +``` + +## Test Organization + +- `tests/smoke//` - Smoke tests by feature +- `tests/unit//` - Unit tests by feature +- `tests/docs//` - Test fixtures + +**Details:** `llm-docs/testing-patterns.md` for comprehensive patterns and examples. diff --git a/.claude/rules/typescript/cliffy-commands.md b/.claude/rules/typescript/cliffy-commands.md new file mode 100644 index 0000000000..62f6f161e4 --- /dev/null +++ b/.claude/rules/typescript/cliffy-commands.md @@ -0,0 +1,48 @@ +--- +paths: + - "src/command/**/*.ts" +--- + +# Cliffy Command Pattern + +Commands use the Cliffy library and follow this structure: + +```typescript +export const myCommand = new Command() + .name("command-name") + .description("What the command does") + .arguments("[input:string]") + .option("-f, --flag", "Flag description") + .option("-o, --output ", "Option with value") + .example("Basic usage", "quarto command input.qmd") + // deno-lint-ignore no-explicit-any + .action(async (options: any, input?: string) => { + // Implementation + }); +``` + +**Options typed as `any`:** Required due to Cliffy limitations. + +**Registration:** Export from `src/command//cmd.ts`, register in `src/command/command.ts`. + +## Error Handling + +Use custom error classes from `src/core/lib/error.ts`: + +```typescript +import { InternalError, ErrorEx, asErrorEx } from "../core/lib/error.ts"; + +// Programming errors (bugs) +throw new InternalError("This should never happen"); + +// User-facing errors +throw new ErrorEx("User-facing error message"); + +// Normalize unknown errors +try { + riskyOperation(); +} catch (e) { + const err = asErrorEx(e); + error(err.message); +} +``` diff --git a/.claude/rules/typescript/deno-essentials.md b/.claude/rules/typescript/deno-essentials.md new file mode 100644 index 0000000000..65d4cc78fe --- /dev/null +++ b/.claude/rules/typescript/deno-essentials.md @@ -0,0 +1,94 @@ +--- +paths: + - "src/**/*.ts" +--- + +# TypeScript/Deno Essentials + +Core conventions for all TypeScript code in quarto-cli. + +## Import Patterns + +### Use Import Map Names + +```typescript +// Correct - use import map names +import { Command } from "cliffy/command/mod.ts"; +import { join, dirname } from "../deno_ral/path.ts"; + +// Wrong - never hardcode JSR/npm URLs +import { join } from "jsr:/@std/path"; +``` + +### Always Include `.ts` Extensions + +```typescript +// Correct +import { debug } from "../../deno_ral/log.ts"; + +// Wrong - no extension +import { debug } from "../../deno_ral/log"; +``` + +## Deno RAL (Runtime Abstraction Layer) + +Import from `src/deno_ral/` instead of standard library directly. See `.claude/rules/typescript/deno-ral.md` for the full module reference, safe file operations, and internals. + +```typescript +// Correct +import { join, dirname } from "../deno_ral/path.ts"; +import { existsSync } from "../deno_ral/fs.ts"; + +// Wrong - direct std lib import +import { join } from "jsr:/@std/path"; +``` + +## Deno APIs vs Node.js + +Use Deno APIs directly: + +```typescript +// Correct - Deno APIs +Deno.env.get("PATH"); +Deno.cwd(); +Deno.readTextFileSync(path); +Deno.writeTextFileSync(path, content); +Deno.makeTempDirSync({ prefix: "quarto" }); + +// Wrong - Node.js patterns +process.env.PATH; +fs.readFileSync(path); +``` + +## Sync vs Async + +Prefer sync APIs in CLI handlers for simpler code flow. Use async for I/O-heavy parallel operations. + +## Path Handling + +```typescript +import { normalizePath } from "../core/path.ts"; +import { isWindows } from "../deno_ral/platform.ts"; + +// Normalize paths for consistent comparison +const normalized = normalizePath(Deno.cwd()); + +// Platform-specific logic +if (isWindows) { + // Windows-specific handling +} +``` + +## Lint Directives + +Common directives (use sparingly): +- `no-explicit-any` - For Cliffy options, JSON parsing +- `no-control-regex` - For regex with control characters + +## Key Conventions + +1. **Use import map names** - Never hardcode JSR/npm URLs +2. **Include `.ts` extensions** - Required for Deno resolution +3. **Import from deno_ral** - Not directly from std library +4. **Use Deno APIs** - No Node.js equivalents +5. **Prefer sync APIs** - Simpler code flow for CLI diff --git a/.claude/rules/typescript/deno-ral.md b/.claude/rules/typescript/deno-ral.md new file mode 100644 index 0000000000..b0678808f3 --- /dev/null +++ b/.claude/rules/typescript/deno-ral.md @@ -0,0 +1,66 @@ +--- +paths: + - "src/deno_ral/**/*.ts" +--- + +# Deno RAL (Runtime Abstraction Layer) + +The `deno_ral/` directory provides a runtime abstraction layer over Deno's standard library. All code should import from here instead of directly from std. + +## Available Modules + +| Module | Purpose | Key exports | +|--------|---------|-------------| +| `fs.ts` | File system | `existsSync`, `ensureDirSync`, `copySync`, `safeMoveSync`, `safeRemoveSync` | +| `path.ts` | Path utilities | `join`, `dirname`, `basename`, `extname` | +| `log.ts` | Logging | `debug`, `info`, `warning`, `error` | +| `platform.ts` | Platform detection | `isWindows` | +| `process.ts` | Process execution | Process utilities | + +## Why Use deno_ral + +- Consistent API across the codebase +- Abstraction allows future runtime changes +- Import map resolution works correctly +- Avoids scattered `jsr:/@std/*` imports + +## Common Utilities + +**Temp files** (`src/core/temp.ts`): +```typescript +import { createTempContext } from "../core/temp.ts"; + +const temp = createTempContext(); +try { + const tempFile = temp.createFile({ suffix: ".json" }); +} finally { + temp.cleanup(); +} +``` + +**Process execution** (`src/core/process.ts`): +```typescript +import { execProcess } from "../core/process.ts"; + +const result = await execProcess({ + cmd: ["pandoc", "--version"], + stdout: "piped", +}); +``` + +## Safe File Operations + +`deno_ral/fs.ts` provides safe wrappers over raw Deno APIs. Prefer these: + +- **`safeMoveSync(src, dest)`** — Use instead of `Deno.renameSync`. Handles cross-device moves by falling back to copy+delete on `EXDEV` errors. +- **`safeRemoveSync(path, options)`** — Use instead of `Deno.removeSync`. Tolerates already-removed paths (no error if file doesn't exist). +- **`safeRemoveDirSync(path, boundary)`** — Safe recursive removal that refuses to delete outside the boundary directory. + +## Module Loading Order + +`src/quarto.ts` loads monkey patches first: +```typescript +import "./core/deno/monkey-patch.ts"; // Must be first! +``` + +This ensures compatibility shims load before any code runs. diff --git a/.gitignore b/.gitignore index a4012725b0..a86ca1548c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,13 @@ .DS_Store .idea + +# Claude Code files +CLAUDE.local.md +.claude/commands/ +.claude/docs/ +.claude/settings.local.json +.claude/.claude/ +.claude/.private-journal/ *.RData *.Rproj *.Rhistory diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4805db862d..d2693fe317 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -31,3 +31,7 @@ Pull requests are very welcome! Here's how to contribute via PR: 3. Submit the [pull request](https://help.github.com/articles/using-pull-requests). It is ok to submit as draft if your are still working on it but would like some feedback from us. It is always good to share in the open that you are working on it. We'll try to be as responsive as possible in reviewing and accepting pull requests. + +## AI-Assisted Development + +This repository includes shared [Claude Code](https://docs.anthropic.com/en/docs/claude-code) memory files in `.claude/` that help AI assistants understand the Quarto codebase. See [dev-docs/claude-code-setup.md](dev-docs/claude-code-setup.md) for details on how these work and how to contribute to them. diff --git a/dev-docs/claude-code-setup.md b/dev-docs/claude-code-setup.md new file mode 100644 index 0000000000..cc1e48ea5e --- /dev/null +++ b/dev-docs/claude-code-setup.md @@ -0,0 +1,89 @@ +# Claude Code Setup for Quarto Contributors + +This project includes shared [Claude Code](https://docs.anthropic.com/en/docs/claude-code) memory files that help AI assistants understand the Quarto codebase. These files are committed to the repository so all contributors benefit from consistent AI-assisted development. + +## What's Included + +### `.claude/CLAUDE.md` + +Always loaded by Claude Code. Contains the project overview: architecture, setup, build commands, conventions, and key file paths. + +### `.claude/rules/` + +Path-scoped rule files that load conditionally based on what files you're working with. For example, when editing Lua filters, Claude Code automatically loads filter-specific conventions. + +Current rule areas: +- `changelog.md` — Changelog entry format +- `filters/` — Lua filter coding conventions and system overview +- `formats/` — Format handler patterns +- `rendering/` — Render pipeline architecture +- `schemas/` — Zod schema patterns +- `testing/` — Test infrastructure, smoke-all format, Playwright, anti-patterns +- `typescript/` — Deno essentials, RAL, Cliffy commands +- `dev-tools/` — Development commands reference +- `llm-docs-maintenance.md` — LLM documentation staleness checking + +Each rule file has a `paths:` frontmatter that controls when it loads: + +```yaml +--- +paths: + - "src/resources/filters/**" +--- +``` + +This means the file only loads when Claude Code is working with files matching those paths. + +### `llm-docs/` + +Architectural deep-dive documentation for AI assistants. These are NOT auto-loaded — they're read on demand when Claude Code needs detailed understanding of a subsystem. Topics include template systems, error messages, testing patterns, and Lua API reference. + +Each llm-doc has staleness metadata in its frontmatter so Claude Code can check if the documented code has changed since the analysis was done. + +## Personal Overrides + +Create `CLAUDE.local.md` at the repository root for personal overrides. This file is gitignored and won't be committed. Use it for: + +- Preferred shell syntax or platform-specific notes +- Personal workflow customizations +- References to personal tools or configurations + +Claude Code loads `CLAUDE.local.md` alongside the project `CLAUDE.md`. + +## Adding or Updating Rules + +### New rule file + +1. Create `.claude/rules//rule-name.md` +2. Add `paths:` frontmatter listing glob patterns relative to the repo root (e.g., `"src/resources/filters/**"`) +3. Keep rules focused and concise (50-250 lines is typical) + +### Update existing rule + +Edit the relevant file in `.claude/rules/`. The path scoping ensures changes only affect sessions working with matching files. + +### New llm-doc + +1. Create `llm-docs/topic-name.md` +2. Add staleness frontmatter (`main_commit`, `analyzed_date`, `key_files`) +3. Reference from relevant rule files if helpful + +## What's NOT Committed + +The `.gitignore` excludes personal Claude Code files: + +``` +CLAUDE.local.md # Personal overrides +.claude/commands/ # Personal slash commands +.claude/docs/ # Personal documentation +.claude/settings.local.json # Local settings +``` + +These stay personal to each developer. + +Avoid committing API keys, tokens, or credentials in any `.claude/` or `llm-docs/` file. Use environment variables or `.env` (also gitignored) for sensitive values. + +## Further Reading + +- [Claude Code documentation](https://docs.anthropic.com/en/docs/claude-code) +- [Memory files reference](https://docs.anthropic.com/en/docs/claude-code/memory) diff --git a/dev-docs/debugging-flaky-tests.md b/dev-docs/debugging-flaky-tests.md new file mode 100644 index 0000000000..67e30dc33e --- /dev/null +++ b/dev-docs/debugging-flaky-tests.md @@ -0,0 +1,247 @@ +# Debugging Flaky Tests: A Systematic Approach + +A methodology for debugging tests that fail intermittently in CI or when run as part of a test suite, but pass when run in isolation. + +## Problem Pattern + +Tests that hang or timeout when run in CI or as part of a test suite, but work fine when run alone. Common root cause categories: + +1. **State pollution**: One test modifies global state that affects subsequent tests +2. **Resource leaks**: File handles, processes, or network connections not cleaned up +3. **Environment corruption**: Package managers (TinyTeX, npm, etc.) get into inconsistent state +4. **Timing/race conditions**: Tests depend on specific execution order or timing + +## Investigation Methodology + +### Phase 1: Reproduce Locally + +**Goal**: Confirm you can reproduce the issue outside of CI. + +1. Identify the failing test bucket from CI logs +2. Extract the test file list from CI configuration +3. Create a test script to run the bucket sequentially: + +```bash +# array.sh - Run tests sequentially +readarray -t my_array < <(echo '[...]' | jq -rc '.[]') +haserror=0 +for file in "${my_array[@]}"; do + echo ">>> ./run-tests.sh ${file}" + shopt -s globstar && ./run-tests.sh $file + status=$? + [ $status -eq 0 ] && echo ">>> No error" || haserror=1 +done +``` + +4. Run and confirm the hang occurs locally + +### Phase 2: Binary Search to Isolate Culprit + +**Goal**: Find which specific test file causes the issue. + +If test N causes state pollution, tests 1 through N-1 will pass, then the problematic test will occur. + +1. Split your test list in half +2. Run first half + the hanging test: + +```bash +# test-binary-search.sh +readarray -t tests < <(echo '[first_half_tests, "hanging-test.qmd"]' | jq -rc '.[]') +for file in "${tests[@]}"; do + ./run-tests.sh $file || exit 1 +done +``` + +3. If it hangs: culprit is in first half, repeat with first half +4. If it passes: culprit is in second half, repeat with second half +5. Continue until you identify the single test file + +**Example**: In [#13647](https://github.com/quarto-dev/quarto-cli/issues/13647), binary search across 51 tests identified `render-format-extension.test.ts` as the culprit. + +### Phase 3: Narrow Down Within Test File + +**Goal**: Find which specific operation in the test file causes pollution. + +1. Read the test file to understand what it does +2. Identify distinct operations (e.g., rendering different formats) +3. Comment out sections and retest: + +```typescript +// Comment out formats one by one to isolate +// test("academic/document.qmd elsevier-pdf", ...) +// test("academic/document.qmd springer-pdf", ...) +test("academic/document.qmd acm-pdf", ...) +``` + +4. Binary search through the operations to find the specific one + +**Example**: In #13647, rendering `academic/document.qmd` with `elsevier-pdf` format was the specific trigger. + +### Phase 4: Understand the State Change + +**Goal**: Determine what environmental change causes the issue. + +Common suspects: package installations (TinyTeX, npm, pip), configuration file modifications, cache pollution, file system changes. + +1. Create a clean test environment (fresh TinyTeX install) +2. Take snapshots before/after the problematic operation: + +```bash +# Before snapshot +tlmgr list --only-installed > before.txt + +# Run problematic test +./run-tests.sh problematic-test.ts + +# After snapshot +tlmgr list --only-installed > after.txt +diff before.txt after.txt +``` + +3. For TinyTeX issues, check: + - Installed packages: `tlmgr list --only-installed` + - Package versions: `tlmgr info ` + - Format files: `ls -la $(kpsewhich -var-value TEXMFSYSVAR)/web2c/luatex/` + - What `tlmgr update --all` installs + +**Example**: In #13647, `elsevier-pdf` rendering triggered `tlmgr update --all` which updated core packages and regenerated lualatex format files. The format regeneration expected modern conventions that conflicted with the bundled class file. + +### Phase 5: Identify Root Cause + +**Goal**: Understand WHY the state change causes the failure. + +1. Compare working vs broken states in detail +2. For package version issues: + - Check if test bundles old versions of libraries/classes + - Compare with system-installed versions + - Review changelogs between versions +3. Create minimal reproduction: + +```bash +# verify-root-cause.sh +echo "=== Test 1: Old version ===" +# Setup with old version, run problematic operation, run hanging test + +echo "=== Test 2: New version ===" +# Setup with new version, run problematic operation, run hanging test +``` + +**Example**: In #13647, the bundled `elsarticle.cls v3.3` was missing `\RequirePackage[T1]{fontenc}`. TinyTeX's `elsarticle.cls v3.4c` includes it. The font encoding mismatch corrupted lualatex format files, causing subsequent lualatex renders to hang. + +### Phase 6: Verify Solution + +**Goal**: Confirm your fix resolves the issue. + +1. Apply the fix (update package, patch code, etc.) +2. Create verification script: + +```bash +#!/bin/bash +echo ">>> Fresh environment setup" +# Clean install + +echo ">>> Running problematic test (with fix)" +./run-tests.sh problematic-test.ts || exit 1 + +echo ">>> Testing previously-hanging test" +./run-tests.sh hanging-test.qmd || exit 1 + +echo "SUCCESS: Fix verified!" +``` + +3. Run multiple times to ensure consistency +4. Test with clean environment each time (critical for environment pollution issues) + +## Key Debugging Tools + +### TinyTeX + +```bash +# List installed packages +tlmgr list --only-installed + +# Check package info +tlmgr info + +# Find file locations +kpsewhich elsarticle.cls + +# Check format files +ls -la $(kpsewhich -var-value TEXMFSYSVAR)/web2c/luatex/ + +# Clean TinyTeX (for fresh start) +rm -rf ~/.TinyTeX +quarto install tinytex +``` + +### Test Isolation + +```bash +# Run single test +./run-tests.sh path/to/test.ts + +# Run test sequence to reproduce ordering issues +for test in test1.ts test2.ts test3.ts; do + ./run-tests.sh $test || break +done +``` + +### Package/Dependency Comparison + +```bash +# Compare package versions +npm list +tlmgr list --only-installed +pip list + +# Check for bundled vs system versions +find . -name "*.cls" -o -name "*.sty" +``` + +## Best Practices + +1. **Always reproduce locally first** - CI is too slow for iterative debugging +2. **Use binary search** - Most efficient way to isolate culprits in large test suites +3. **Test with clean environments** - Especially for environment pollution issues +4. **Take snapshots** - Before/after comparisons are invaluable +5. **Create verification scripts** - Automate testing your fix +6. **Document the root cause** - Help others understand the issue + +## Common Pitfalls + +1. **Testing with polluted environment** - Always start fresh for environment issues +2. **Assuming causation from correlation** - Just because test A runs before test B doesn't mean A causes B's failure +3. **Stopping too early** - Finding the problematic test isn't enough; understand WHY it causes issues +4. **Not verifying the fix** - Always confirm your solution actually works + +## Checklist + +- [ ] Reproduce the issue locally +- [ ] Identify the specific test bucket that triggers the issue +- [ ] Use binary search to isolate the culprit test file +- [ ] Narrow down to specific operation within the test +- [ ] Take environment snapshots before/after +- [ ] Identify what environmental change occurs +- [ ] Understand WHY the change causes the failure +- [ ] Develop and apply a fix +- [ ] Verify the fix with clean environments +- [ ] Document the root cause and solution + +## Case Study: #13647 (tufte.qmd Hanging in CI) + +**Symptom**: `tufte.qmd` hangs after 10+ minutes when run after a bucket of tests. Same document renders fine in ~30s when run alone. Lualatex engine stuck during "running lualatex - 1". + +**Investigation summary**: + +```bash +# Binary search: 51 tests → render-format-extension.test.ts +# Narrow down: elsevier-pdf format was the trigger +# State change: tlmgr update --all regenerated format files +# Root cause: Bundled elsarticle.cls v3.3 missing fontenc, corrupting lualatex formats +# Fix: Update extension to use elsarticle.cls v3.4c +``` + +**References**: +- [quarto-dev/quarto-cli#13647](https://github.com/quarto-dev/quarto-cli/issues/13647) +- [quarto-journals/elsevier#38](https://github.com/quarto-journals/elsevier/pull/38) - Update elsarticle.cls +- [quarto-journals/elsevier#40](https://github.com/quarto-journals/elsevier/pull/40) - CTAN update diff --git a/llm-docs/callout-styling-html.md b/llm-docs/callout-styling-html.md new file mode 100644 index 0000000000..8496237189 --- /dev/null +++ b/llm-docs/callout-styling-html.md @@ -0,0 +1,228 @@ +--- +main_commit: 50db5a5b0 +analyzed_date: 2026-01-22 +key_files: + - src/resources/formats/html/bootstrap/_bootstrap-rules.scss + - src/resources/formats/revealjs/quarto.scss + - src/resources/formats/html/styles-callout.html + - src/resources/filters/customnodes/callout.lua +--- + +# HTML Callout Styling Architecture + +This document describes the CSS architecture for Quarto callouts across HTML-based output formats. + +## Overview + +Quarto uses a **three-tier callout styling architecture** depending on the output format: + +| Tier | Formats | CSS Location | Features | +|------|---------|--------------|----------| +| Bootstrap HTML | `html` (with themes) | `formats/html/bootstrap/_bootstrap-rules.scss` | Full theming, collapsible, dark mode | +| RevealJS | `revealjs` | `formats/revealjs/quarto.scss` | Presentation-specific scaling, slide-aware | +| Standalone HTML | `epub`, `gfm`, plain html | `formats/html/styles-callout.html` | Inline CSS, no dependencies | + +All HTML callouts support three **appearance** values: +- **default**: Full-featured with colored header background +- **simple**: Lightweight with left border only +- **minimal**: Maps to simple with `icon=false` + +## Format Detection (Lua Filter) + +The Lua filter `src/resources/filters/customnodes/callout.lua` selects the appropriate renderer: + +``` +Renderer selection order: +1. hasBootstrap() → Bootstrap HTML renderer +2. isEpubOutput() || isRevealJsOutput() → Simpler HTML structure +3. isGfmOutput() → GitHub markdown alerts +4. Default → BlockQuote fallback +``` + +The `hasBootstrap()` function (in `filters/common/pandoc.lua`) checks the `has-bootstrap` parameter set by TypeScript during format initialization. + +## HTML Structure by Format + +### Bootstrap HTML + +```html +
+
+
+
Title
+
+
+
Content
+
+
+``` + +### EPUB/RevealJS HTML + +```html +
+
+
+
Title
+
Content
+
+
+``` + +Note: Bootstrap uses `callout-body-container` wrapper and Bootstrap utility classes (`d-flex`, `flex-fill`). EPUB/RevealJS uses a flatter structure. + +## Feature Comparison + +| Feature | Bootstrap | RevealJS | Standalone | +|---------|-----------|----------|------------| +| Collapsible | Yes | No | No | +| Icon type | SVG (dynamic color) | SVG (dynamic color) | PNG (base64) | +| Theming | Full Bootstrap vars | Presentation vars | Fixed colors | +| Dark mode | Yes | Slide background aware | No | +| Font scaling | Responsive | Presentation-specific (0.7em) | Fixed (0.9rem) | + +--- + +## Bootstrap HTML Styling + +File: `src/resources/formats/html/bootstrap/_bootstrap-rules.scss` + +### Callout States + +| State | CSS Class | Description | +|-------|-----------|-------------| +| Titled | `.callout-titled` | Has a title/header | +| Untitled | `:not(.callout-titled)` | Content only, no header | +| Collapsed | `.callout-header.collapsed` | Collapsible, currently closed | +| Empty content | `.callout-empty-content` | No body content | + +### Styling Patterns + +**Base callout:** +```scss +.callout { + margin-top: $callout-margin-top; + margin-bottom: $callout-margin-bottom; + border-radius: $border-radius; +} +``` + +**Simple vs Default appearance:** +- `.callout-style-simple`: Left border only, lighter styling +- `.callout-style-default`: Full border, colored header background + +**Body margins** vary by appearance (simple/default) and titled state (titled/untitled). The margin rules handle edge cases like collapsed callouts and empty content states. + +### Theming Variables + +Bootstrap callouts use SCSS variables (in `_bootstrap-variables.scss`): + +```scss +$callout-border-width: 0.4rem !default; +$callout-border-scale: 0% !default; +$callout-icon-scale: 10% !default; +$callout-margin-top: 1.25rem !default; +$callout-margin-bottom: 1.25rem !default; +``` + +Colors are defined per callout type (note, warning, important, tip, caution) using Bootstrap's color functions. + +--- + +## RevealJS Styling + +File: `src/resources/formats/revealjs/quarto.scss` + +### Presentation-Specific Adjustments + +```scss +// Variables +$callout-border-width: 0.3rem; +$callout-margin-top: 1rem; +$callout-margin-bottom: 1rem; + +// Font scaling for slide readability +.reveal div.callout { + font-size: 0.7em; +} +``` + +### Light/Dark Slide Awareness + +RevealJS callouts adjust colors based on slide background using the `shift_to_dark` mixin: + +```scss +.has-dark-background div.callout-note { + // Lighter colors for dark backgrounds +} +``` + +--- + +## Standalone/EPUB Styling + +File: `src/resources/formats/html/styles-callout.html` + +### Characteristics + +- **Inline CSS** embedded in HTML template +- **PNG icons** (base64-encoded) instead of SVG +- **Fixed colors**: Uses hardcoded `#acacac`, `silver` borders +- **No collapsible support** +- **No theming** - works without Bootstrap or any CSS framework + +### Key Selectors + +```css +.callout /* Base container */ +.callout.callout-style-simple /* Simple bordered style */ +.callout.callout-style-default /* Default style with header */ +.callout-title /* Title container */ +.callout-body /* Content container */ +.callout-icon::before /* Icon pseudo-element */ +``` + +--- + +## CSS Class Reference + +Classes applied across all HTML formats: + +| Class | Applied When | Purpose | +|-------|--------------|---------| +| `.callout` | Always | Base container | +| `.callout-{type}` | Always | Type: note, warning, important, tip, caution | +| `.callout-style-{appearance}` | Always | Style: default, simple | +| `.callout-titled` | Has title | Structural indicator | +| `.no-icon` | `icon=false` | Suppress icon | +| `.callout-empty-content` | No body | Empty state (Bootstrap only) | + +--- + +## Related Files + +### CSS/SCSS + +| File | Purpose | +|------|---------| +| `src/resources/formats/html/bootstrap/_bootstrap-rules.scss` | Bootstrap HTML callout styles | +| `src/resources/formats/html/bootstrap/_bootstrap-variables.scss` | Bootstrap callout variables | +| `src/resources/formats/revealjs/quarto.scss` | RevealJS callout styles | +| `src/resources/formats/html/styles-callout.html` | Standalone HTML template | +| `src/resources/formats/dashboard/quarto-dashboard.scss` | Dashboard margin overrides | + +### Lua Filters + +| File | Purpose | +|------|---------| +| `src/resources/filters/customnodes/callout.lua` | Renderer selection and AST processing | +| `src/resources/filters/modules/callouts.lua` | Bootstrap renderer implementation | +| `src/resources/filters/common/pandoc.lua` | `hasBootstrap()` function | + +### Tests + +| File | Purpose | +|------|---------| +| `tests/docs/callouts.qmd` | All callout types and appearances | +| `tests/docs/playwright/html/callouts/` | Playwright test documents | +| `tests/integration/playwright/tests/html-callouts.spec.ts` | Playwright CSS tests | diff --git a/llm-docs/testing-patterns.md b/llm-docs/testing-patterns.md new file mode 100644 index 0000000000..66661c2340 --- /dev/null +++ b/llm-docs/testing-patterns.md @@ -0,0 +1,425 @@ +# Quarto Test Patterns + +This document describes the standard patterns for writing smoke tests in the Quarto CLI test suite. + +## Test Structure Overview + +Quarto uses Deno for testing with custom verification helpers located in: + +- `tests/test.ts` - Core test runner (`testQuartoCmd`) +- `tests/verify.ts` - Verification helpers (`fileExists`, `pathDoNotExists`, etc.) +- `tests/utils.ts` - Utility functions (`docs()`, `outputForInput()`, etc.) + +## Common Test Patterns + +### Simple Render Tests + +For testing single document rendering with automatic cleanup: + +```typescript +import { docs } from "../../utils.ts"; +import { testRender } from "./render.ts"; + +// Simplest form - just render and verify output created +testRender(docs("test-plain.md"), "html", false); +``` + +**With additional verifiers:** + +```typescript +import { docs, outputForInput } from "../../utils.ts"; +import { testRender } from "./render.ts"; +import { ensureHtmlElements } from "../../verify.ts"; + +const input = docs("minimal.qmd"); +const output = outputForInput(input, "html"); + +testRender(input, "html", true, [ + ensureHtmlElements(output.outputPath, [], ["script#quarto-html-after-body"]), +]); +``` + +**Key points:** + +- `testRender()` automatically handles output verification and cleanup +- Respects `QUARTO_TEST_KEEP_OUTPUTS` env var for debugging +- Set `noSupporting` parameter based on expected output: + - `true` - For truly self-contained HTML (no `_files/` directory, inline everything) + - `false` - For HTML with supporting files directory (OJS runtime, widget dependencies, plots, etc.) + - Most HTML outputs should use `false` (only use `true` for formats like `html` with `self-contained: true`) +- Pass additional verifiers in the array parameter (optional) +- Cleanup happens automatically via `cleanoutput()` in teardown + +### Project Rendering Tests + +For testing project rendering (especially website projects): + +```typescript +import { docs } from "../../utils.ts"; +import { join } from "../../../src/deno_ral/path.ts"; +import { existsSync } from "../../../src/deno_ral/fs.ts"; +import { testQuartoCmd } from "../../test.ts"; +import { fileExists, pathDoNotExists, noErrors } from "../../verify.ts"; + +const projectDir = docs("project/my-test"); // Relative path via docs() +const outputDir = join(projectDir, "_site"); // Append output dir for websites + +testQuartoCmd( + "render", + [projectDir], + [ + noErrors, // Check for no errors + fileExists(join(outputDir, "index.html")), // Verify expected file exists + pathDoNotExists(join(outputDir, "ignored.html")), // Verify file doesn't exist + ], + { + teardown: async () => { + if (existsSync(outputDir)) { + await Deno.remove(outputDir, { recursive: true }); + } + }, + }, +); +``` + +**Key points:** + +- Use `docs()` helper to create relative paths from `tests/docs/` +- For website projects, output goes to `_site` subdirectory +- Use absolute paths with `join()` for file verification +- Clean up output directories in teardown + +### Extension Template Tests + +For testing `quarto use template`: + +```typescript +import { testQuartoCmd } from "../../test.ts"; +import { + fileExists, + noErrorsOrWarnings, + pathDoNotExists, +} from "../../verify.ts"; +import { join } from "../../../src/deno_ral/path.ts"; +import { ensureDirSync } from "../../../src/deno_ral/fs.ts"; + +const tempDir = Deno.makeTempDirSync(); + +// Create mock template source +const templateSourceDir = join(tempDir, "template-source"); +ensureDirSync(templateSourceDir); +Deno.writeTextFileSync(join(templateSourceDir, "template.qmd"), "..."); +Deno.writeTextFileSync(join(templateSourceDir, "config.yml"), "..."); + +const templateFolder = "my-test-template"; +const workingDir = join(tempDir, templateFolder); +ensureDirSync(workingDir); + +testQuartoCmd( + "use", + ["template", templateSourceDir, "--no-prompt"], + [ + noErrorsOrWarnings, + fileExists(`${templateFolder}.qmd`), // Relative - template file renamed to folder name + pathDoNotExists(join(workingDir, "README.md")), // Absolute - excluded file + ], + { + cwd: () => workingDir, // Set working directory + teardown: () => { + try { + Deno.removeSync(tempDir, { recursive: true }); + } catch { + // Ignore cleanup errors + } + return Promise.resolve(); + }, + }, +); +``` + +**Key points:** + +- Use `Deno.makeTempDirSync()` for isolated test environment +- Create mock template source with test files +- Template files get renamed to match target directory name +- Use `cwd()` function to set working directory for command execution +- Clean up entire temp directory including source files +- File verification uses relative paths when checking files in `cwd()` + +## Verification Helpers + +### Core Verifiers + +```typescript +// No errors in output +noErrors + +// No errors or warnings +noErrorsOrWarnings + +// File exists at path +fileExists(path: string) + +// Path does not exist +pathDoNotExists(path: string) + +// Folder exists at path +folderExists(path: string) + +// Directory contains only allowed paths +directoryEmptyButFor(dir: string, allowedFiles: string[]) +``` + +### Path Helpers + +```typescript +// Create relative path from tests/docs/ +docs(path: string): string +// Example: docs("project/site") → "tests/docs/project/site" + +// Get expected output for input file +outputForInput(input: string, to: string, projectOutDir?: string, projectRoot?: string) + +// Find project directory +findProjectDir(input: string, until?: RegExp) + +// Find project output directory (_site, _book, etc.) +findProjectOutputDir(projectdir: string) +``` + +## Output Directory Patterns + +Different project types use different output directories: + +```typescript +// Website project +const outputDir = join(projectDir, "_site"); + +// Book project +const outputDir = join(projectDir, "_book"); + +// Manuscript project +const outputDir = join(projectDir, "_manuscript"); + +// Plain project (no type specified) +// Output goes directly in project directory +const outputDir = projectDir; +``` + +## Test File Organization + +Tests follow this directory structure: + +``` +tests/ +├── docs/ # Test fixtures +│ └── project/ +│ └── my-test/ +│ ├── _quarto.yml +│ ├── index.qmd +│ └── other-files.qmd +├── smoke/ # Smoke tests +│ ├── project/ +│ │ └── project-my-test.test.ts +│ ├── render/ +│ ├── use/ +│ └── ... +├── test.ts # Test runner +├── verify.ts # Verification helpers +└── utils.ts # Utility functions +``` + +## Common Patterns + +### Cleanup Pattern + +Always clean up generated files in teardown: + +```typescript +teardown: async () => { + if (existsSync(outputPath)) { + await Deno.remove(outputPath, { recursive: true }); + } +}; +``` + +### Multiple Test Cases + +When testing multiple scenarios, declare constants at module level: + +```typescript +const tempDir = Deno.makeTempDirSync(); + +// Test case 1 +const folder1 = "test-case-1"; +const workingDir1 = join(tempDir, folder1); +ensureDirSync(workingDir1); +testQuartoCmd(...); + +// Test case 2 +const folder2 = "test-case-2"; +const workingDir2 = join(tempDir, folder2); +ensureDirSync(workingDir2); +testQuartoCmd(...); +``` + +### Path Construction + +- **Absolute paths**: Use `join()` for all path operations +- **Relative to docs**: Use `docs()` helper +- **Relative to cwd**: Use plain strings or template literals in template tests + +## Examples from Codebase + +### Simple Render Test + +See `tests/smoke/render/render-plain.test.ts` for the simplest render tests (no additional verifiers). + +See `tests/smoke/render/render-minimal.test.ts` for render test with custom HTML element verification. + +### Project Ignore Test + +See `tests/smoke/project/project-ignore-dirs.test.ts` for testing directory exclusion patterns. + +### Website Rendering Test + +See `tests/smoke/project/project-website.test.ts` for website project rendering patterns. + +### Template Usage Test + +See `tests/smoke/use/template.test.ts` for extension template patterns. + +## Engine-Specific Test Considerations + +### Shared Test Environments (Critical for quarto-cli Testing) + +**Quarto-cli test infrastructure uses a SINGLE managed environment for all tests:** + +- **Julia**: `tests/Project.toml` + `tests/Manifest.toml` +- **Python**: `tests/.venv/` (managed by uv/pyproject.toml) +- **R**: `tests/renv/` + `tests/renv.lock` + +The `configure-test-env` scripts ONLY manage these main environments. CI builds depend on this structure. + +**Do NOT create language environment files in test subdirectories:** + +``` +tests/docs/my-test/ +├── Project.toml # ❌ WRONG - breaks test infrastructure +├── .venv/ # ❌ WRONG - breaks test infrastructure +├── renv.lock # ❌ WRONG - breaks test infrastructure +└── test.qmd +``` + +**Why this fails:** + +- Julia searches UP for `Project.toml` and uses the first one found +- Python/R will use local environments if present +- CI scripts won't configure these local environments +- Tests will fail in CI even if they work locally + +**Adding new package dependencies:** + +For ANY engine (Julia, Python, R), add dependencies to the main `tests/` environment: + +```bash +# Julia: Use Pkg from tests/ directory +cd tests +julia --project=. -e 'using Pkg; Pkg.add("PackageName")' +# Then run configure to update environment +./configure-test-env.sh # or .ps1 on Windows + +# Python: Use uv from tests/ directory +cd tests +uv add packagename + +# R: Edit tests/DESCRIPTION, then +cd tests +Rscript -e "renv::install(); renv::snapshot()" +``` + +**Note:** While Quarto supports local Project.toml files in document directories for production use, the quarto-cli test infrastructure specifically does NOT support this pattern. All test dependencies must be in the main `tests/` environment. + +## Best Practices + +1. **Always clean up**: Use teardown to remove generated files +2. **Use helpers**: Leverage `docs()`, `fileExists()`, etc. instead of manual checks +3. **Absolute paths**: Use `join()` for all path construction to handle platform differences +4. **Test isolation**: Use temp directories for tests that create files +5. **Clear names**: Use descriptive variable names like `projectDir`, `outputDir`, `templateFolder` +6. **Comment intent**: Add comments explaining what should/shouldn't happen +7. **Handle errors**: Wrap cleanup in try-catch to avoid test suite failures from cleanup issues + +## Environment Variable Testing Pitfalls + +`Deno.env.set()` modifies process-global state. Deno runs test files in parallel by default (same OS process), so concurrent tests can see modified values. Save/restore patterns don't help - other tests see the modified value during the test window. + +| Execution Mode | Risk | Why | +| -------------------------- | ------------------ | --------------------------------------- | +| `./run-tests.sh` (default) | **Race condition** | Files run in parallel, share `Deno.env` | +| `./run-parallel-tests.sh` | **None** | Separate OS processes | + +**Existing bad pattern** - `tests/smoke/website/drafts-env.test.ts`: + +```typescript +// BAD: Sets env var, never restores it +// Only "works" because no other test reads QUARTO_PROFILE +Deno.env.set("QUARTO_PROFILE", "drafts"); +testQuartoCmd("render", [renderDir], [...]); +``` + +**Alternatives:** Unit test the env var reader, refactor code to accept parameters, or use subprocess isolation. + +## Testing File Exclusion + +When testing that files are excluded (like AI config files): + +```typescript +// Test that files are NOT rendered +testQuartoCmd( + "render", + [projectDir], + [ + noErrors, + fileExists(join(outputDir, "expected.html")), // Should exist + pathDoNotExists(join(outputDir, "excluded.html")), // Should NOT exist + ], + // ... +); +``` + +Run test **without fix** first to verify it fails, then verify it passes with fix. + +## Smoke-All Tests (YAML-Based) + +Smoke-all tests embed test specifications directly in `.qmd` files using `_quarto.tests` metadata. See `.claude/rules/testing/smoke-all-tests.md` for full documentation. + +### YAML String Escaping for Regex + +**Critical rule:** In YAML single-quoted strings, `'\('` and `"\\("` are equivalent - both produce a literal `\(` in the regex. + +**Common mistake:** Over-escaping with `'\\('` produces `\\(` (two backslashes), causing regex to fail. + +```yaml +_quarto: + tests: + pdf: + ensureLatexFileRegexMatches: + # CORRECT - single backslash in YAML single quotes + - ['\(1\)', '\\circled\{1\}', "Variable assignment"] + - ['\\CommentTok', '\\begin\{Shaded\}'] + + # WRONG - over-escaped (produces \\( in regex) + - ['\\(1\\)', '\\\\circled\\{1\\}'] +``` + +**YAML escaping cheat sheet:** + +| To match in file | In single quotes `'...'` | In double quotes `"..."` | +| ---------------- | ------------------------ | ------------------------ | +| `\(` | `'\('` | `"\\("` | +| `\begin{` | `'\\begin\{'` | `"\\\\begin\\{"` | +| `\\` (literal) | `'\\\\'` | `"\\\\\\\\"` | +| `[` (regex) | `'\['` | `"\\["` | + +**Recommendation:** Use single-quoted strings. They're simpler - only `'` itself needs escaping (as `''`).