Repository navigation
feat(directives): generic tutorial directive grammar foundation (openspec: tutorial-directive-grammar) - #3021
Merged
Merged
Conversation
…tutorial-directive-grammar)
…sertion (review round 1)
…g), C2 injection tests, I1 real recursion, M1 cleanup
Insert expandDirectives(body, {slug, variables}) between the wrapTokens
block and convertOptionBlocks in composeTutorial. The call is the only
production-code change; with an empty registry it is an identity
transform, so tutorials without directives are unaffected.
Five integration tests in directives-compose.test.ts cover: plain-step
render, variable-in-label (proves AFTER wrapTokens), directive-inside-
OPTION-panel (proves BEFORE convertOptionBlocks), code-fence passthrough,
and empty-registry identity.
Regression: variables.test.ts (34 tests) and v2.test.ts (9 tests) green.
Adopt renderDirectivesAsText (Task 3) at three read sites so models and MCP clients see readable prose instead of raw :::details / :kbd[ syntax. Sites adopted: - packages/mcp/tutorial-step-slicer.js: apply in loadAndParseMarkdown before the step entry is built; import follows the existing srv/lib pattern (../../scripts/parsers/directives.js → .ts resolved at runtime). - srv/lib/freshness-detector.js: strip directives from src.markdown before passing to extractCodeBlocks / extractTutorialContext; same import pattern. - scripts/lib/expand-ai-authored.ts: strip directives from stepBody when read from stepBodies (step-source path only; video path overwrites below). All three sites: empty registry → identity transform (no registered directives yet → safe no-op on current content). Tests: extended directives-text.test.ts with MCP read-path contract (:::details[Why] + :kbd[Mod+S] → **Why** + Mod+S, no raw syntax) and identity check. Slicer and expand-ai-authored unit suites all pass (9/9 and 32/32 respectively).
…ote/backslash attr escaping (final review)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What & why
Implements the
tutorial-directive-grammarOpenSpec change (proposed in #3018) — the foundation the other 7 markdown-syntax features build on. Adds one generic CommonMark-style directive grammar to tutorial markdown::name[label]{attrs}, leaf::name[label]{attrs}, container:::name[label]{attrs}…:::(nest with more colons)compose.ts(afterwrapTokens, beforeconvertOptionBlocks) rewriting directives intotut-<name>Hugo shortcodesrenderDirectivesAsText()fallback adopted by the MCP step slicer, freshness detector, and AI-authoring read pathsdirective-unknownlint warning, andnpm run build:directive-schema→schemas/tutorial-directives.schema.jsonfor authoring toolsShips ZERO concrete directives — the 7 follow-up features register directives on top. Core guarantee: byte-identical output for tutorials that don't use directives, proven by a corpus-regression test.
Security: escaping is safe-by-construction
DirectiveDef.renderreturns a structured{ params, body?, markdown? }; the framework (buildShortcode) HTML-escapes every param + label (incl. backslash) — a directive author cannot emit an unescaped value. This invariant is inherited by all fan-out features.Type of change
How tested
openspec validate tutorial-directive-grammar --strict→ valid.wrapTokens), directives inside OPTION panels render in the right tab (runs beforeconvertOptionBlocks), directive-looking text inside code fences is untouched, and empty-registry output is byte-identical (toBe).npm testin a fresh unbuilt worktree has pre-existing env failures unrelated to this change (missing compiledsap-fundamental.css, missingsrv/lib/_shared/{content,core}.bundle.mjs, aparseMarkdownSteps {time=}failure owned by a pre-existing feat(tutorials): per-step time/optional + interactive checklists (openspec: tutorial-step-pacing) #3019 commit). All directive tests are green.Review notes
:::foono longer throws under an empty registry (byte-identity safety); (2) attribute tokenizer now supportskey='value'and backslash-escaped quotes, andescapeShortcodeParamescapes backslash.directives-render.test.ts, the shared container-close/inline-column fallbacks in the two back-to-front replacers.\) — directive families with backslash-bearing attrs should decode in their template if needed.Deploy scope
srv/**,scripts/**) + docs — no content rebuild, no admin UI, no Hugo asset change. Parser/renderer only; empty registry = no runtime behavior change.Checklist
DEVhugo/content/tutorials/docs/authors/writing-tutorials.md§Directives + gotcha)