Skip to content

@gram-lang/renderer

Renders a CompilationResult or AnalyzedCompilationResult to Markdown, HTML, or a print-optimized standalone HTML document. If you’re building a custom UI instead (React, Vue, Svelte), you likely don’t need this package at all — consume the JSON directly, see How to Build a Custom UI.

type RenderableCompilationResult = CompilationResult | AnalyzedCompilationResult;

function toMarkdown(data: RenderableCompilationResult, options?: RendererOptions): string
function toHTML(data: RenderableCompilationResult, options?: RendererOptions): string
function toPrintHTML(data: RenderableCompilationResult, options?: RendererOptions): string
import { compile } from '@gram-lang/kitchen';
import { toHTML } from '@gram-lang/renderer';

const compiled = compile(ast);
const html = toHTML(compiled, { lang: 'en' });

toPrintHTML returns a complete, self-contained HTML document (inline <style>, A4 @page rules, no external stylesheet dependency) suitable for “print this recipe” / PDF-export features — toHTML returns a bare fragment meant to be embedded into an existing page.

All three formatters share a single traversal architecture (RenderBackend), ensuring features like nutrition summaries, footnotes, gross mass badges, and mixed-unit warnings render consistently across Markdown, HTML, and Print output.

OptionTypeDescription
iconsRendererIconsOverride any subset of the default icon glyphs (see DEFAULT_ICONS below).
classesRendererClassesOverride CSS class names on generated elements (HTML/print only).
formatFraction(value: number) => stringCustom decimal → fraction formatter (default: common fractions like 0.5"1/2").
formatDuration(minutes: number) => stringCustom duration formatter (default: e.g. 90"1h 30m").
hideStepQtybooleanOmit ingredient quantities from inline step text across all formatters (shopping list and mise-en-place are unaffected).
bakersMathOnlybooleanShow only baker’s percentages, hiding absolute quantities.
interactiveScalingbooleanRender interactive portion/ingredient scaling controls (HTML only).
nutritionBasis'auto' | 'total' | 'perPortion' | 'per100g'Which nutrition basis to display. 'auto' (the default) shows per-portion when the recipe declares a portion count, otherwise the whole recipe.
interactiveNutritionbooleanHTML only: emit every available nutrition basis behind a CSS-only reader toggle instead of a single one. Requires the renderer stylesheet; ignored when nutritionBasis pins a basis.
langstringLocale code (e.g. 'en', 'fr') for translating UI strings, via @gram-lang/i18n’s dictionaries.
renderIdstringPrefix for footnote anchor ids — override when rendering multiple recipes on one page to avoid id collisions.

Gantt chart (toGanttHTML & attachGanttInteractivity)

Section titled “Gantt chart (toGanttHTML & attachGanttInteractivity)”

Renders a compiled/analyzed recipe into an interactive timeline view, offering a precise visual and temporal representation of preparation steps, active tasks, and background timers.

import { toGanttHTML, attachGanttInteractivity } from '@gram-lang/renderer';

// 1. Generate the static HTML fragment
const ganttHtml = toGanttHTML(compiled, { lang: 'en' });
container.innerHTML = ganttHtml;

// 2. Attach interactive controls and hover tooltips
const handle = attachGanttInteractivity(container, {
  timeMode: 'forward',   // 'forward' (T+ stopwatch), 'reverse' (T- countdown), or 'target' (target time)
  targetTime: '19:30',   // Target serve time (HH:MM)
  isCompactMode: false   // Toggle compact row height
});

// Update or query options dynamically
handle.setOptions({ isCompactMode: true });

// Clean up event listeners on unmount
handle.dispose();
OptionTypeDescription
langstringLocale code (e.g. 'en', 'fr') for UI translations via @gram-lang/i18n.
gapThresholdMinutesnumberMinimum idle gap duration in minutes before gap compression is applied (default: 60).
compressedGapSizenumberVirtual minute width that compressed idle gaps collapse down to (default: 20).
OptionTypeDescription
timeMode'forward' | 'reverse' | 'target'Timeline tick display mode: elapsed time (T+), countdown (T-), or clock time based on serve target.
targetTimestringTarget serve time formatted as "HH:MM".
isCompactModebooleanToggles compact view mode for tight vertical space.
import { DEFAULT_ICONS, toHTML } from '@gram-lang/renderer';

const html = toHTML(compiled, {
  icons: { ...DEFAULT_ICONS.html, clock: '<svg class="my-clock-icon">...</svg>' },
});

DEFAULT_ICONS has two variants, DEFAULT_ICONS.html (Phosphor <i> tags) and DEFAULT_ICONS.md (emoji), each keyed by a subset of RendererIcons: hourglass, timer, thermometer, caretRight, arrowRight, arrowUDownLeft, warning, pencilSimple, minus, plus. The remaining RendererIcons fields (clock, fire, knife, scales, clockCounterClockwise, arrowElbowDownRight, info) aren’t part of DEFAULT_ICONStoHTML falls back to its own hardcoded Phosphor markup for those when options.icons doesn’t override them, so overriding one of these seven only has an effect when passed directly via options.icons, not via a spread of DEFAULT_ICONS.

Lower-level helpers used internally by the three formatters, exported for building custom renderers on top of the same conventions:

function formatDecimalToFraction(value: unknown): string   // 0.5 -> "1/2"
function getQty(item: Record<string, unknown>): { value: number | string | null; text?: string; isRelative?: boolean } | undefined
function formatQuantityValue(q: any): string                // Timer/Temperature quantity -> display string
function formatDuration(minutes: number): string            // 90 -> "1h 30m"
function escapeHtml(unsafe: string | null | undefined): string
function escapeMarkdownHtml(unsafe: string | null | undefined): string   // neutralizes `<`/`&` for safe Markdown-to-HTML rendering downstream
function joinStepTokens(tokens: StepToken[], renderToken: (token: StepToken) => string, isSpaceable: (token: StepToken) => boolean): string