From 7db317e1d5da84a41b2e26960550e4433706285c Mon Sep 17 00:00:00 2001 From: Aliaksandr Kukrash Date: Tue, 7 Jul 2026 19:52:02 +0200 Subject: [PATCH 01/12] Version 2 upgrade Signed-off-by: Aliaksandr Kukrash --- .gitattributes | 11 + .github/workflows/docs.yml | 59 +- .github/workflows/dotnet.yml | 10 +- .github/workflows/lsp.yml | 95 + .github/workflows/samples.yml | 50 + .gitignore | 19 + Directory.Build.props | 5 + Heddle.sln | 201 + docs/.vitepress/config.mts | 6 +- docs/README.md | 5 +- docs/assessment.md | 472 +- docs/building.md | 27 +- docs/built-in-extensions.md | 224 +- docs/csharp-api.md | 124 +- docs/custom-extensions.md | 212 +- docs/demo-smoke/demo.spec.ts | 99 + docs/editor-support.md | 127 + docs/language-reference.md | 291 +- docs/native-expressions.md | 215 + docs/package-lock.json | 64 + docs/package.json | 4 +- docs/patterns.md | 103 +- docs/playwright.config.ts | 24 + docs/precompilation.md | 216 + docs/public/demo.html | 334 +- docs/roadmap/README.md | 156 + docs/roadmap/phase-1-native-expressions.md | 492 ++ docs/roadmap/phase-2-safe-output.md | 407 ++ docs/roadmap/phase-3-branching.md | 496 ++ docs/roadmap/phase-4-ergonomics.md | 300 ++ docs/roadmap/phase-5-props-and-slots.md | 350 ++ docs/roadmap/phase-6-tooling-lsp.md | 413 ++ .../roadmap/phase-7-build-time-compilation.md | 664 +++ docs/roadmap/phase-8-streaming-async.md | 358 ++ docs/roadmap/phase-9-demo-and-integration.md | 257 + docs/roadmap/plan.md | 167 + docs/spec/OPEN-QUESTIONS.md | 76 + docs/spec/README.md | 59 + docs/spec/common/coding-standards.md | 215 + docs/spec/common/cross-cutting-decisions.md | 204 + docs/spec/common/migration-2.0.md | 104 + docs/spec/common/spec-conventions.md | 172 + docs/spec/common/testing-standards.md | 88 + .../spec/phase-1-native-expressions/README.md | 1020 ++++ .../phase-1-native-expressions/grammar.md | 405 ++ .../operator-semantics.md | 281 ++ docs/spec/phase-2-safe-output/README.md | 823 ++++ docs/spec/phase-3-branching/README.md | 1087 +++++ docs/spec/phase-3-branching/entry-points.md | 140 + docs/spec/phase-4-ergonomics/README.md | 1168 +++++ docs/spec/phase-5-props-and-slots/README.md | 1285 +++++ docs/spec/phase-5-props-and-slots/grammar.md | 490 ++ docs/spec/phase-6-tooling-lsp/README.md | 1783 +++++++ docs/spec/phase-6-tooling-lsp/protocol.md | 589 +++ .../phase-7-build-time-compilation/README.md | 1659 +++++++ .../generated-code.md | 1440 ++++++ .../identity-and-metadata.md | 378 ++ docs/spec/phase-8-streaming-async/README.md | 1444 ++++++ .../phase-9-demo-and-integration/README.md | 882 ++++ .../phase-9-demo-and-integration/gallery.md | 312 ++ .../traceability-run.md | 59 + .../phase-9-demo-and-integration/wasm-demo.md | 447 ++ editors/vscode/build/copy-grammar.js | 17 + editors/vscode/language-configuration.json | 20 + editors/vscode/package-lock.json | 4276 +++++++++++++++++ editors/vscode/package.json | 109 + editors/vscode/src/extension.ts | 119 + editors/vscode/src/test/smoke.test.ts | 25 + .../vscode/syntaxes/heddle.tmLanguage.json | 168 + editors/vscode/tsconfig.json | 16 + samples/Heddle.Samples.slnx | 35 + samples/README.md | 62 + .../CodegenT4Successor.csproj | 45 + samples/codegen-t4-successor/Program.cs | 76 + samples/codegen-t4-successor/README.md | 39 + .../golden/codegen-output.txt | 6 + .../golden/dependency-report.txt | 2 + .../golden/generated-source.cs.txt | 74 + .../templates/report.heddle | 6 + .../ComponentPropsSlots.csproj | 18 + samples/component-props-slots/Program.cs | 89 + samples/component-props-slots/README.md | 40 + .../golden/components-page.html | 7 + .../custom-extensions/ChannelExtensions.cs | 65 + .../custom-extensions/CustomExtensions.csproj | 18 + samples/custom-extensions/Program.cs | 56 + samples/custom-extensions/README.md | 37 + samples/custom-extensions/golden/branch.txt | 2 + samples/custom-extensions/golden/channel.txt | 1 + .../DefinitionLibrary.csproj | 18 + samples/definition-library/Program.cs | 56 + samples/definition-library/README.md | 36 + samples/definition-library/golden/page.html | 7 + .../templates/library.heddle | 8 + .../definition-library/templates/page.heddle | 12 + samples/dynamic-models/DynamicModels.csproj | 18 + samples/dynamic-models/Program.cs | 88 + samples/dynamic-models/README.md | 33 + .../dynamic-models/golden/dynamic-anon.txt | 6 + .../golden/dynamic-dictionary.txt | 5 + .../html-safe-output/HtmlSafeOutput.csproj | 18 + samples/html-safe-output/Program.cs | 65 + samples/html-safe-output/README.md | 36 + samples/html-safe-output/TaggingExtension.cs | 40 + .../golden/custom-encoder.html | 1 + .../html-safe-output/golden/profile-html.html | 2 + .../html-safe-output/golden/profile-text.html | 2 + samples/precompiled-app/PrecompiledApp.csproj | 51 + samples/precompiled-app/Program.cs | 69 + samples/precompiled-app/README.md | 38 + .../precompiled-app/golden/differential.txt | 2 + samples/precompiled-app/golden/discovery.txt | 2 + .../golden/precompiled-output.html | 4 + .../precompiled-app/templates/invoice.heddle | 4 + .../precompiled-app/templates/receipt.heddle | 3 + samples/sandboxed-user-templates/Program.cs | 130 + samples/sandboxed-user-templates/README.md | 41 + .../SandboxedUserTemplates.csproj | 18 + .../golden/rejected.txt | 3 + .../golden/rendered/arithmetic.txt | 1 + .../golden/rendered/conditional.txt | 1 + .../golden/rendered/member-path.txt | 1 + .../golden/rendered/whitelisted.txt | 1 + samples/ssr-aspnetcore/Program.cs | 112 + samples/ssr-aspnetcore/README.md | 35 + samples/ssr-aspnetcore/SsrAspNetCore.csproj | 17 + samples/ssr-aspnetcore/golden/article-1.html | 8 + samples/ssr-aspnetcore/golden/index.html | 11 + .../ssr-aspnetcore/templates/_footer.heddle | 1 + .../ssr-aspnetcore/templates/_header.heddle | 1 + .../ssr-aspnetcore/templates/article.heddle | 7 + samples/ssr-aspnetcore/templates/index.heddle | 9 + samples/streaming-ssr/Program.cs | 105 + samples/streaming-ssr/README.md | 34 + samples/streaming-ssr/StreamingSsr.csproj | 17 + samples/streaming-ssr/golden/streamed.html | 6 + samples/streaming-ssr/golden/textwriter.html | 6 + samples/tools/SampleCapture.cs | 58 + samples/tools/compare-golden-selftest.sh | 39 + samples/tools/compare-golden.sh | 69 + src/Heddle.Demo.Models/BlogModel.cs | 39 + src/Heddle.Demo.Models/DemoCatalog.cs | 114 + .../Heddle.Demo.Models.csproj | 13 + src/Heddle.Demo.Wasm/DemoHost.cs | 158 + src/Heddle.Demo.Wasm/DemoInterop.cs | 42 + src/Heddle.Demo.Wasm/DemoJsonContext.cs | 18 + src/Heddle.Demo.Wasm/DemoProtocol.cs | 64 + src/Heddle.Demo.Wasm/Heddle.Demo.Wasm.csproj | 27 + src/Heddle.Demo.Wasm/Program.cs | 5 + .../contract-fixtures/complete-c01.json | 18 + .../contract-fixtures/complete-c02-root.json | 15 + .../contract-fixtures/complete-c04-props.json | 14 + .../complete-c09-stepback.json | 15 + .../diagnostic-member-typo.json | 15 + .../contract-fixtures/render-blog.json | 18 + .../linker/Heddle.Demo.roots.xml | 14 + src/Heddle.Demo.Wasm/stage_demo.sh | 18 + .../wwwroot/heddle-demo-worker.js | 31 + src/Heddle.Demo.Wasm/wwwroot/main.js | 14 + .../BranchTests.cs | 78 + .../CSharpVerbatimTests.cs | 69 + .../CompositionTests.cs | 60 + .../CorpusDifferentialTests.cs | 157 + .../CorpusRenderParityTests.cs | 81 + .../CustomExtensionTests.cs | 78 + .../DefinitionTests.cs | 91 + .../DifferentialHarness.cs | 296 ++ .../DifferentialRenderTests.cs | 72 + .../DynamicTierTests.cs | 62 + .../ExportFunctionTests.cs | 72 + .../Fixtures/CustomExtensions.cs | 48 + .../Fixtures/Models.cs | 84 + .../Fixtures/TemplateFunctions.cs | 22 + .../ForTests.cs | 56 + .../FunctionTests.cs | 57 + .../Heddle.Generator.IntegrationTests.csproj | 25 + .../ListTests.cs | 51 + .../NativeExpressionTests.cs | 72 + .../PartialTests.cs | 139 + .../PrecompiledSinkTests.cs | 150 + .../ProfileFlipTests.cs | 53 + .../PropsTests.cs | 115 + .../ResolverIntegrationTests.cs | 98 + .../SlotAndDefaultOutputTests.cs | 88 + .../StartupProbeTests.cs | 60 + .../TrimTests.cs | 53 + .../UnresolvableFunctionTests.cs | 57 + .../Utf8PieceTests.cs | 55 + .../GeneratorHarness.cs | 174 + .../GeneratorSnapshotTests.cs | 111 + .../Heddle.Generator.Tests.csproj | 31 + .../HeddleGeneratorTests.cs | 291 ++ ...ple1_StaticTextAndMemberPaths.verified.txt | 105 + ...sts.Example2_NativeExpression.verified.txt | 96 + ...ample3_EmbeddedCSharpVerbatim.verified.txt | 96 + ...ests.Example4_DynamicTemplate.verified.txt | 93 + ....Example5_DefinitionWithProps.verified.txt | 21 + ...pshotTests.Example6_BranchSet.verified.txt | 135 + ...sts.Example7_FunctionShimCall.verified.txt | 21 + ...7b_UnresolvableFunctionMarker.verified.txt | 39 + .../Binding/FunctionExportResolver.cs | 140 + .../Binding/SymbolMemberResolver.cs | 62 + .../Binding/SymbolTypeResolver.cs | 249 + .../Diagnostics/GeneratorDiagnostics.cs | 121 + src/Heddle.Generator/Emit/CodeWriter.cs | 39 + src/Heddle.Generator/Emit/DocumentShaper.cs | 386 ++ src/Heddle.Generator/Emit/ExtensionBinder.cs | 140 + src/Heddle.Generator/Emit/LineMapper.cs | 54 + src/Heddle.Generator/Emit/MemberPathWriter.cs | 68 + .../Emit/NativeExpressionWriter.cs | 307 ++ src/Heddle.Generator/Emit/PieceWriter.cs | 74 + src/Heddle.Generator/Emit/TemplateEmitter.cs | 2109 ++++++++ src/Heddle.Generator/Heddle.Generator.csproj | 76 + .../HeddleTemplateGenerator.cs | 490 ++ src/Heddle.Generator/Pipeline/ConfigReader.cs | 81 + src/Heddle.Generator/Pipeline/GlobalConfig.cs | 60 + .../build/Heddle.Generator.props | 31 + .../build/Heddle.Generator.targets | 18 + src/Heddle.Language/Heddle.Language.csproj | 1 + src/Heddle.Language/HeddleLexer.g4 | 112 +- src/Heddle.Language/HeddleParser.g4 | 69 +- src/Heddle.Language/generated/HeddleLexer.cs | 1753 ++++--- .../generated/HeddleLexer.interp | 180 +- .../generated/HeddleLexer.tokens | 129 +- src/Heddle.Language/generated/HeddleParser.cs | 2601 ++++++++-- .../generated/HeddleParser.interp | 139 +- .../generated/HeddleParser.tokens | 129 +- .../generated/HeddleParserBaseListener.cs | 414 ++ .../generated/HeddleParserListener.cs | 352 ++ src/Heddle.Language/js/HeddleLexer.interp | 180 +- src/Heddle.Language/js/HeddleLexer.js | 2259 +++++---- src/Heddle.Language/js/HeddleLexer.tokens | 129 +- src/Heddle.Language/js/HeddleParser.interp | 139 +- src/Heddle.Language/js/HeddleParser.js | 3136 ++++++++++-- src/Heddle.Language/js/HeddleParser.tokens | 129 +- .../js/HeddleParserListener.js | 279 ++ .../Heddle.LanguageServer.csproj | 45 + src/Heddle.LanguageServer/LspServer.cs | 376 ++ src/Heddle.LanguageServer/Program.cs | 47 + src/Heddle.LanguageServer/Protocol/Dtos.cs | 151 + .../Protocol/LspJsonContext.cs | 35 + .../SemanticTokensBuilder.cs | 98 + .../Corpus.cs | 61 + ...eddle.LanguageServices.Tests.Corpus.csproj | 17 + .../AssemblyInfo.cs | 4 + .../Corpus/broken-lib.heddle | 1 + .../Corpus/layout.heddle | 3 + .../Corpus/typo-lib.heddle | 3 + .../CorpusFixture.cs | 52 + .../DemoContractTests.cs | 166 + .../ExportScanTests.cs | 55 + .../Heddle.LanguageServices.Tests.csproj | 50 + .../LanguageServiceCompletionTests.cs | 119 + .../LanguageServiceDefinitionTests.cs | 50 + .../LanguageServiceDiagnosticsTests.cs | 83 + .../LanguageServiceHoverTests.cs | 54 + .../ModelAssemblyReloadTests.cs | 48 + .../PositionMappingTests.cs | 53 + .../ProtocolContractTests.cs | 160 + .../ServerLifecycleTests.cs | 87 + .../Completion/CompletionProvider.cs | 199 + .../Completion/CompletionText.cs | 132 + .../Completion/ContextDetector.cs | 194 + .../Completion/DefinitionProvider.cs | 43 + .../Completion/HoverProvider.cs | 122 + .../CompletionResult.cs | 89 + src/Heddle.LanguageServices/DefinitionInfo.cs | 63 + .../DocumentAnalysis.cs | 46 + .../DocumentAnalyzer.cs | 277 ++ .../ExtensionRegistrar.cs | 156 + .../FunctionExportRegistrar.cs | 54 + .../Heddle.LanguageServices.csproj | 34 + .../HeddleDiagnostic.cs | 43 + .../HeddleLanguageService.cs | 184 + .../HeddleLanguageServiceOptions.cs | 26 + src/Heddle.LanguageServices/ImportLink.cs | 31 + src/Heddle.LanguageServices/LineMap.cs | 71 + .../ModelAssemblyContext.cs | 68 + .../ModelAssemblyManager.cs | 59 + src/Heddle.LanguageServices/ScopeMapView.cs | 61 + .../WorkspaceConfig.cs | 91 + .../BranchRenderBenchmarks.cs | 66 + .../LanguageServiceBenchmarks.cs | 48 + src/Heddle.Performance/Program.cs | 8 + .../PropsRenderBenchmarks.cs | 115 + .../SinkRenderBenchmarks.cs | 108 + .../TemplateParseBenchmarks.cs | 55 + src/Heddle.Tests/AssemblyInfo.cs | 6 + src/Heddle.Tests/BranchAllocationTests.cs | 96 + src/Heddle.Tests/BranchConcurrencyTests.cs | 77 + src/Heddle.Tests/BranchIsolationTests.cs | 176 + src/Heddle.Tests/BranchProtocolTests.cs | 212 + src/Heddle.Tests/BranchSetCompilerTests.cs | 284 ++ src/Heddle.Tests/BranchTestExtensions.cs | 135 + src/Heddle.Tests/BranchingGoldenTests.cs | 137 + src/Heddle.Tests/Data/ErgoForData.cs | 8 + .../DefaultFunctionLockstepTests.cs | 83 + src/Heddle.Tests/DiagnosticIdTests.cs | 73 + src/Heddle.Tests/DoubleEncodeWarningTests.cs | 82 + src/Heddle.Tests/DoubleRenderWarningTests.cs | 128 + src/Heddle.Tests/ExpressionModeTests.cs | 108 + src/Heddle.Tests/FeatureSwitchTests.cs | 125 + src/Heddle.Tests/ForSugarAllocationTests.cs | 61 + src/Heddle.Tests/ForSugarTests.cs | 119 + src/Heddle.Tests/FunctionExportTests.cs | 138 + src/Heddle.Tests/FunctionRegistryTests.cs | 142 + src/Heddle.Tests/Heddle.Tests.csproj | 197 + src/Heddle.Tests/HeddleTemplateTests.cs | 9 + src/Heddle.Tests/ImportFormsPinningTests.cs | 108 + src/Heddle.Tests/ImportOriginTests.cs | 109 + .../NativeExpressionConcurrencyTests.cs | 90 + .../NativeExpressionGoldenTests.cs | 113 + .../NativeExpressionOperatorTests.cs | 156 + .../NativeExpressionParseTests.cs | 317 ++ .../NativeExpressionSandboxTests.cs | 177 + .../NativeExpressionSmokeTests.cs | 100 + .../OutputProfileEncodingTests.cs | 296 ++ src/Heddle.Tests/OutputProfileGoldenTests.cs | 82 + src/Heddle.Tests/OutputProfileOptionsTests.cs | 84 + src/Heddle.Tests/ParserSettingsSeamTests.cs | 98 + src/Heddle.Tests/PrecompiledGauntletTests.cs | 211 + .../PrecompiledMismatchPolicyTests.cs | 46 + src/Heddle.Tests/PrecompiledRegistryTests.cs | 168 + src/Heddle.Tests/PrecompiledRuntimeTests.cs | 126 + src/Heddle.Tests/ProfileDirectiveTests.cs | 130 + src/Heddle.Tests/PropsBindingTests.cs | 207 + src/Heddle.Tests/PropsConcurrencyTests.cs | 102 + src/Heddle.Tests/PropsGoldenTests.cs | 68 + src/Heddle.Tests/PropsInheritanceTests.cs | 188 + src/Heddle.Tests/PropsModels.cs | 48 + src/Heddle.Tests/PropsParseTests.cs | 333 ++ src/Heddle.Tests/PropsShadowingTests.cs | 65 + src/Heddle.Tests/PublicApiSurfaceTests.cs | 136 + src/Heddle.Tests/RangeFunctionTests.cs | 93 + .../ScopeChannelDocExampleTests.cs | 88 + src/Heddle.Tests/ScopeChannelTests.cs | 164 + src/Heddle.Tests/ScopeMapTests.cs | 76 + src/Heddle.Tests/SlotProjectionTests.cs | 153 + .../Streaming/FormatterSpanGoldenTests.cs | 117 + .../Streaming/SinkAdapterTests.cs | 142 + .../Streaming/SinkEncodingParityTests.cs | 58 + src/Heddle.Tests/Streaming/SinkParityTests.cs | 150 + src/Heddle.Tests/Streaming/SinkTestHarness.cs | 64 + src/Heddle.Tests/Streaming/SinkTestWriters.cs | 144 + .../StreamingFixtureAndGuaranteeTests.cs | 217 + .../Streaming/Utf8ChunkBoundaryTests.cs | 81 + .../TemplateKeyNormalizationTests.cs | 74 + .../TemplateOptionsCompletenessTests.cs | 117 + .../TemplateResolverProfileTests.cs | 115 + .../TestTemplate/branch-import-def.heddle | 1 + .../TestTemplate/branch-import-else.heddle | 1 + .../TestTemplate/branching-flagship.heddle | 3 + .../TestTemplate/branching-interleaved.heddle | 1 + .../branching-list-alternating.heddle | 1 + .../TestTemplate/branching-nested.heddle | 1 + .../branching-out-projection.heddle | 1 + .../branching-partial-child.heddle | 1 + .../branching-partial-parent.heddle | 1 + .../TestTemplate/ergo-double-render.heddle | 5 + src/Heddle.Tests/TestTemplate/ergo-for.heddle | 5 + .../TestTemplate/ergo-import-broken.heddle | 1 + .../ergo-import-composition.heddle | 4 + .../TestTemplate/ergo-import-empty.heddle | 4 + .../TestTemplate/ergo-import-inline.heddle | 3 + .../TestTemplate/ergo-import-library.heddle | 8 + .../TestTemplate/ergo-trim-preamble.heddle | 9 + .../TestTemplate/expr-flagship.heddle | 6 + .../TestTemplate/expr-functions.heddle | 12 + ...generated-branching-flagship-archived.html | 1 + ...generated-branching-flagship-featured.html | 1 + .../generated-branching-flagship-regular.html | 1 + .../generated-branching-list-alternating.html | 1 + .../generated-branching-nested.html | 1 + .../generated-branching-out-projection.html | 1 + .../generated-branching-partial.html | 1 + .../generated-ergo-double-render.html | 3 + .../TestTemplate/generated-ergo-for.html | 5 + .../generated-ergo-import-composition.html | 5 + .../TestTemplate/generated-ergo-trim-off.html | 6 + .../TestTemplate/generated-ergo-trim-on.html | 1 + .../TestTemplate/generated-expr-flagship.html | 6 + .../generated-expr-functions.html | 12 + .../generated-profile-directive.html | 1 + .../generated-profile-flagship-html.html | 3 + .../generated-profile-flagship-text.html | 3 + .../generated-profile-partial.html | 1 + .../generated-props-abstract-panel.html | 2 + .../TestTemplate/generated-props-card.html | 4 + .../generated-props-defaults.html | 2 + .../TestTemplate/generated-props-inherit.html | 2 + .../TestTemplate/generated-slot-compose.html | 2 + .../TestTemplate/generated-slot-picker.html | 2 + .../generated-streaming-unicode.html | 6 + .../TestTemplate/import-origin-a.heddle | 1 + .../TestTemplate/import-origin-b.heddle | 1 + .../import-origin-badmember-lib.heddle | 3 + .../TestTemplate/import-origin-broken.heddle | 1 + .../TestTemplate/import-origin-c.heddle | 1 + .../TestTemplate/profile-directive.heddle | 1 + .../TestTemplate/profile-flagship.heddle | 3 + .../TestTemplate/profile-partial-child.heddle | 1 + .../profile-partial-parent.heddle | 1 + .../profile-resolver-default.heddle | 1 + .../TestTemplate/props-abstract-panel.heddle | 4 + .../TestTemplate/props-card.heddle | 6 + .../TestTemplate/props-defaults.heddle | 8 + .../TestTemplate/props-inherit.heddle | 5 + .../public-api-heddle-language.txt | 908 ++++ .../TestTemplate/public-api-heddle.txt | 829 ++++ .../TestTemplate/slot-compose.heddle | 5 + .../TestTemplate/slot-picker.heddle | 4 + .../TestTemplate/streaming-large.heddle | 16 + .../TestTemplate/streaming-unicode.heddle | 6 + src/Heddle.Tests/TrimDirectiveLinesTests.cs | 132 + .../Heddle.Tool.Tests.csproj | 22 + src/Heddle.Tool.Tests/HeddleToolTests.cs | 122 + src/Heddle.Tool/Heddle.Tool.csproj | 46 + src/Heddle.Tool/HeddleRenderer.cs | 81 + src/Heddle.Tool/Program.cs | 112 + .../Attributes/ExportFunctionsAttribute.cs | 35 + .../Attributes/ExtensionNameAttribute.cs | 7 +- .../Attributes/ScopeChannelAttribute.cs | 17 + src/Heddle/Core/AbstractExtension.cs | 48 +- src/Heddle/Core/DefinitionBaseExtension.cs | 92 +- src/Heddle/Core/InitContext.cs | 7 + src/Heddle/Core/SlotContent.cs | 23 + src/Heddle/Data/BranchState.cs | 33 + src/Heddle/Data/CompileError.cs | 19 + src/Heddle/Data/ExpressionMode.cs | 17 + src/Heddle/Data/HeddleCompileError.cs | 23 +- src/Heddle/Data/HeddleCompileResult.cs | 11 - src/Heddle/Data/HeddleDiagnosticIds.cs | 168 + src/Heddle/Data/HtmlEncodedRenderer.cs | 23 +- src/Heddle/Data/ISpanScopeRenderer.cs | 16 + src/Heddle/Data/IUtf8ScopeRenderer.cs | 17 + src/Heddle/Data/LinePosition.cs | 16 + src/Heddle/Data/OutputProfile.cs | 18 + src/Heddle/Data/PrecompiledMismatchPolicy.cs | 23 + src/Heddle/Data/Scope.cs | 177 +- src/Heddle/Data/ScopeLocals.cs | 58 + src/Heddle/Data/ScopeRendererExtensions.cs | 61 + src/Heddle/Data/TemplateOptions.cs | 80 +- src/Heddle/Data/TextWriterScopeRenderer.cs | 52 + src/Heddle/Data/Utf8ScopeRenderer.cs | 114 + src/Heddle/Extensions/BranchCondition.cs | 16 + src/Heddle/Extensions/DateExtension.cs | 4 + src/Heddle/Extensions/ElifExtension.cs | 56 + src/Heddle/Extensions/ElseExtension.cs | 52 + src/Heddle/Extensions/EmptyExtension.cs | 1 + src/Heddle/Extensions/ForIndexExtension.cs | 41 +- src/Heddle/Extensions/GuidExtension.cs | 10 +- src/Heddle/Extensions/IfExtension.cs | 20 +- src/Heddle/Extensions/IfNotExtension.cs | 19 +- src/Heddle/Extensions/ImportExtension.cs | 38 +- src/Heddle/Extensions/IntegerExtension.cs | 15 + src/Heddle/Extensions/MoneyExtension.cs | 10 + src/Heddle/Extensions/OutExtension.cs | 115 +- src/Heddle/Extensions/PartialExtension.cs | 22 +- src/Heddle/Extensions/ProfileExtension.cs | 64 + src/Heddle/Extensions/TimeExtension.cs | 4 + src/Heddle/Heddle.csproj | 9 + src/Heddle/HeddleTemplate.cs | 114 +- src/Heddle/ILLink.Substitutions.xml | 16 + src/Heddle/Language/CallParameter.cs | 17 +- src/Heddle/Language/DefinitionItem.cs | 28 +- src/Heddle/Language/DocumentParser.Runtime.cs | 63 + src/Heddle/Language/DocumentParser.cs | 52 +- src/Heddle/Language/Expressions/ExprNode.cs | 145 + .../Language/Expressions/ExprOperator.cs | 36 + .../Expressions/ExpressionAstBuilder.cs | 512 ++ src/Heddle/Language/Expressions/ThisNode.cs | 17 + src/Heddle/Language/HeddleMainListener.cs | 113 +- .../Language/HeddleSyntaxErrorListener.cs | 9 +- src/Heddle/Language/HeddleToken.cs | 6 +- src/Heddle/Language/ImportOrigin.cs | 27 + src/Heddle/Language/NamedArgument.cs | 25 + src/Heddle/Language/OutputItem.cs | 16 + src/Heddle/Language/ParseContext.cs | 236 +- src/Heddle/Language/ParserSettings.cs | 53 + src/Heddle/Language/PropDeclaration.cs | 36 + src/Heddle/Native/AssemblyHelper.cs | 121 +- src/Heddle/Native/RoslynReferenceProvider.cs | 81 + .../Precompiled/DefaultFunctionTable.cs | 92 + .../HeddleCompiledTemplatesAttribute.cs | 27 + .../Precompiled/IHeddleTemplateManifest.cs | 12 + .../Precompiled/PrecompiledCapabilities.cs | 15 + .../PrecompiledExtensionBinding.cs | 19 + .../Precompiled/PrecompiledFallbackEvent.cs | 26 + .../Precompiled/PrecompiledFallbackReason.cs | 26 + .../Precompiled/PrecompiledFunctionBinding.cs | 27 + .../Precompiled/PrecompiledFunctions.cs | 66 + src/Heddle/Precompiled/PrecompiledGauntlet.cs | 221 + src/Heddle/Precompiled/PrecompiledImport.cs | 18 + .../PrecompiledMismatchException.cs | 25 + .../PrecompiledOptionsFingerprint.cs | 26 + .../Precompiled/PrecompiledPropSetter.cs | 27 + .../PrecompiledRegistrationException.cs | 37 + src/Heddle/Precompiled/PrecompiledRuntime.cs | 306 ++ .../Precompiled/PrecompiledTemplateInfo.cs | 78 + .../Precompiled/PrecompiledTemplates.cs | 209 + src/Heddle/Precompiled/TemplateKey.cs | 130 + src/Heddle/Properties/AssemblyInfo.cs | 1 + src/Heddle/Runtime/CSharpContext.cs | 36 +- src/Heddle/Runtime/CompileContext.cs | 58 + src/Heddle/Runtime/ContextCompilation.cs | 26 + .../Runtime/Expressions/BuiltInFunctions.cs | 227 + .../Runtime/Expressions/FunctionEntry.cs | 98 + .../Runtime/Expressions/FunctionRegistry.cs | 216 + .../Runtime/Expressions/MemberPathResolver.cs | 126 + .../Expressions/NativeExpressionCompiler.cs | 1207 +++++ .../Runtime/Expressions/NumericPromotion.cs | 126 + .../Runtime/Expressions/PropConversion.cs | 102 + src/Heddle/Runtime/Expressions/PropLayout.cs | 164 + src/Heddle/Runtime/HeddleCompiler.cs | 940 +++- src/Heddle/Runtime/HeddleFeatures.cs | 31 + src/Heddle/Runtime/IProcessStrategy.cs | 9 +- .../Runtime/Parameters/ChainedParameter.cs | 7 + .../Runtime/Parameters/ConstantParameter.cs | 4 + .../Runtime/Parameters/ModelParameter.cs | 15 +- .../Parameters/PropsCompiledParameter.cs | 26 + .../Runtime/Parameters/PropsSlotParameter.cs | 34 + src/Heddle/Runtime/PropsBinder.cs | 57 + src/Heddle/Runtime/RuntimeDocument.cs | 63 +- src/Heddle/Runtime/ScopeMap.cs | 56 + src/Heddle/Runtime/TemplateFactory.cs | 28 + src/Heddle/Runtime/TemplateResolver.cs | 124 +- src/Heddle/Strings/ExStringBuilder.cs | 25 +- 527 files changed, 71190 insertions(+), 3255 deletions(-) create mode 100644 .github/workflows/lsp.yml create mode 100644 .github/workflows/samples.yml create mode 100644 docs/demo-smoke/demo.spec.ts create mode 100644 docs/editor-support.md create mode 100644 docs/native-expressions.md create mode 100644 docs/playwright.config.ts create mode 100644 docs/precompilation.md create mode 100644 docs/roadmap/README.md create mode 100644 docs/roadmap/phase-1-native-expressions.md create mode 100644 docs/roadmap/phase-2-safe-output.md create mode 100644 docs/roadmap/phase-3-branching.md create mode 100644 docs/roadmap/phase-4-ergonomics.md create mode 100644 docs/roadmap/phase-5-props-and-slots.md create mode 100644 docs/roadmap/phase-6-tooling-lsp.md create mode 100644 docs/roadmap/phase-7-build-time-compilation.md create mode 100644 docs/roadmap/phase-8-streaming-async.md create mode 100644 docs/roadmap/phase-9-demo-and-integration.md create mode 100644 docs/roadmap/plan.md create mode 100644 docs/spec/OPEN-QUESTIONS.md create mode 100644 docs/spec/README.md create mode 100644 docs/spec/common/coding-standards.md create mode 100644 docs/spec/common/cross-cutting-decisions.md create mode 100644 docs/spec/common/migration-2.0.md create mode 100644 docs/spec/common/spec-conventions.md create mode 100644 docs/spec/common/testing-standards.md create mode 100644 docs/spec/phase-1-native-expressions/README.md create mode 100644 docs/spec/phase-1-native-expressions/grammar.md create mode 100644 docs/spec/phase-1-native-expressions/operator-semantics.md create mode 100644 docs/spec/phase-2-safe-output/README.md create mode 100644 docs/spec/phase-3-branching/README.md create mode 100644 docs/spec/phase-3-branching/entry-points.md create mode 100644 docs/spec/phase-4-ergonomics/README.md create mode 100644 docs/spec/phase-5-props-and-slots/README.md create mode 100644 docs/spec/phase-5-props-and-slots/grammar.md create mode 100644 docs/spec/phase-6-tooling-lsp/README.md create mode 100644 docs/spec/phase-6-tooling-lsp/protocol.md create mode 100644 docs/spec/phase-7-build-time-compilation/README.md create mode 100644 docs/spec/phase-7-build-time-compilation/generated-code.md create mode 100644 docs/spec/phase-7-build-time-compilation/identity-and-metadata.md create mode 100644 docs/spec/phase-8-streaming-async/README.md create mode 100644 docs/spec/phase-9-demo-and-integration/README.md create mode 100644 docs/spec/phase-9-demo-and-integration/gallery.md create mode 100644 docs/spec/phase-9-demo-and-integration/traceability-run.md create mode 100644 docs/spec/phase-9-demo-and-integration/wasm-demo.md create mode 100644 editors/vscode/build/copy-grammar.js create mode 100644 editors/vscode/language-configuration.json create mode 100644 editors/vscode/package-lock.json create mode 100644 editors/vscode/package.json create mode 100644 editors/vscode/src/extension.ts create mode 100644 editors/vscode/src/test/smoke.test.ts create mode 100644 editors/vscode/syntaxes/heddle.tmLanguage.json create mode 100644 editors/vscode/tsconfig.json create mode 100644 samples/Heddle.Samples.slnx create mode 100644 samples/README.md create mode 100644 samples/codegen-t4-successor/CodegenT4Successor.csproj create mode 100644 samples/codegen-t4-successor/Program.cs create mode 100644 samples/codegen-t4-successor/README.md create mode 100644 samples/codegen-t4-successor/golden/codegen-output.txt create mode 100644 samples/codegen-t4-successor/golden/dependency-report.txt create mode 100644 samples/codegen-t4-successor/golden/generated-source.cs.txt create mode 100644 samples/codegen-t4-successor/templates/report.heddle create mode 100644 samples/component-props-slots/ComponentPropsSlots.csproj create mode 100644 samples/component-props-slots/Program.cs create mode 100644 samples/component-props-slots/README.md create mode 100644 samples/component-props-slots/golden/components-page.html create mode 100644 samples/custom-extensions/ChannelExtensions.cs create mode 100644 samples/custom-extensions/CustomExtensions.csproj create mode 100644 samples/custom-extensions/Program.cs create mode 100644 samples/custom-extensions/README.md create mode 100644 samples/custom-extensions/golden/branch.txt create mode 100644 samples/custom-extensions/golden/channel.txt create mode 100644 samples/definition-library/DefinitionLibrary.csproj create mode 100644 samples/definition-library/Program.cs create mode 100644 samples/definition-library/README.md create mode 100644 samples/definition-library/golden/page.html create mode 100644 samples/definition-library/templates/library.heddle create mode 100644 samples/definition-library/templates/page.heddle create mode 100644 samples/dynamic-models/DynamicModels.csproj create mode 100644 samples/dynamic-models/Program.cs create mode 100644 samples/dynamic-models/README.md create mode 100644 samples/dynamic-models/golden/dynamic-anon.txt create mode 100644 samples/dynamic-models/golden/dynamic-dictionary.txt create mode 100644 samples/html-safe-output/HtmlSafeOutput.csproj create mode 100644 samples/html-safe-output/Program.cs create mode 100644 samples/html-safe-output/README.md create mode 100644 samples/html-safe-output/TaggingExtension.cs create mode 100644 samples/html-safe-output/golden/custom-encoder.html create mode 100644 samples/html-safe-output/golden/profile-html.html create mode 100644 samples/html-safe-output/golden/profile-text.html create mode 100644 samples/precompiled-app/PrecompiledApp.csproj create mode 100644 samples/precompiled-app/Program.cs create mode 100644 samples/precompiled-app/README.md create mode 100644 samples/precompiled-app/golden/differential.txt create mode 100644 samples/precompiled-app/golden/discovery.txt create mode 100644 samples/precompiled-app/golden/precompiled-output.html create mode 100644 samples/precompiled-app/templates/invoice.heddle create mode 100644 samples/precompiled-app/templates/receipt.heddle create mode 100644 samples/sandboxed-user-templates/Program.cs create mode 100644 samples/sandboxed-user-templates/README.md create mode 100644 samples/sandboxed-user-templates/SandboxedUserTemplates.csproj create mode 100644 samples/sandboxed-user-templates/golden/rejected.txt create mode 100644 samples/sandboxed-user-templates/golden/rendered/arithmetic.txt create mode 100644 samples/sandboxed-user-templates/golden/rendered/conditional.txt create mode 100644 samples/sandboxed-user-templates/golden/rendered/member-path.txt create mode 100644 samples/sandboxed-user-templates/golden/rendered/whitelisted.txt create mode 100644 samples/ssr-aspnetcore/Program.cs create mode 100644 samples/ssr-aspnetcore/README.md create mode 100644 samples/ssr-aspnetcore/SsrAspNetCore.csproj create mode 100644 samples/ssr-aspnetcore/golden/article-1.html create mode 100644 samples/ssr-aspnetcore/golden/index.html create mode 100644 samples/ssr-aspnetcore/templates/_footer.heddle create mode 100644 samples/ssr-aspnetcore/templates/_header.heddle create mode 100644 samples/ssr-aspnetcore/templates/article.heddle create mode 100644 samples/ssr-aspnetcore/templates/index.heddle create mode 100644 samples/streaming-ssr/Program.cs create mode 100644 samples/streaming-ssr/README.md create mode 100644 samples/streaming-ssr/StreamingSsr.csproj create mode 100644 samples/streaming-ssr/golden/streamed.html create mode 100644 samples/streaming-ssr/golden/textwriter.html create mode 100644 samples/tools/SampleCapture.cs create mode 100644 samples/tools/compare-golden-selftest.sh create mode 100644 samples/tools/compare-golden.sh create mode 100644 src/Heddle.Demo.Models/BlogModel.cs create mode 100644 src/Heddle.Demo.Models/DemoCatalog.cs create mode 100644 src/Heddle.Demo.Models/Heddle.Demo.Models.csproj create mode 100644 src/Heddle.Demo.Wasm/DemoHost.cs create mode 100644 src/Heddle.Demo.Wasm/DemoInterop.cs create mode 100644 src/Heddle.Demo.Wasm/DemoJsonContext.cs create mode 100644 src/Heddle.Demo.Wasm/DemoProtocol.cs create mode 100644 src/Heddle.Demo.Wasm/Heddle.Demo.Wasm.csproj create mode 100644 src/Heddle.Demo.Wasm/Program.cs create mode 100644 src/Heddle.Demo.Wasm/contract-fixtures/complete-c01.json create mode 100644 src/Heddle.Demo.Wasm/contract-fixtures/complete-c02-root.json create mode 100644 src/Heddle.Demo.Wasm/contract-fixtures/complete-c04-props.json create mode 100644 src/Heddle.Demo.Wasm/contract-fixtures/complete-c09-stepback.json create mode 100644 src/Heddle.Demo.Wasm/contract-fixtures/diagnostic-member-typo.json create mode 100644 src/Heddle.Demo.Wasm/contract-fixtures/render-blog.json create mode 100644 src/Heddle.Demo.Wasm/linker/Heddle.Demo.roots.xml create mode 100644 src/Heddle.Demo.Wasm/stage_demo.sh create mode 100644 src/Heddle.Demo.Wasm/wwwroot/heddle-demo-worker.js create mode 100644 src/Heddle.Demo.Wasm/wwwroot/main.js create mode 100644 src/Heddle.Generator.IntegrationTests/BranchTests.cs create mode 100644 src/Heddle.Generator.IntegrationTests/CSharpVerbatimTests.cs create mode 100644 src/Heddle.Generator.IntegrationTests/CompositionTests.cs create mode 100644 src/Heddle.Generator.IntegrationTests/CorpusDifferentialTests.cs create mode 100644 src/Heddle.Generator.IntegrationTests/CorpusRenderParityTests.cs create mode 100644 src/Heddle.Generator.IntegrationTests/CustomExtensionTests.cs create mode 100644 src/Heddle.Generator.IntegrationTests/DefinitionTests.cs create mode 100644 src/Heddle.Generator.IntegrationTests/DifferentialHarness.cs create mode 100644 src/Heddle.Generator.IntegrationTests/DifferentialRenderTests.cs create mode 100644 src/Heddle.Generator.IntegrationTests/DynamicTierTests.cs create mode 100644 src/Heddle.Generator.IntegrationTests/ExportFunctionTests.cs create mode 100644 src/Heddle.Generator.IntegrationTests/Fixtures/CustomExtensions.cs create mode 100644 src/Heddle.Generator.IntegrationTests/Fixtures/Models.cs create mode 100644 src/Heddle.Generator.IntegrationTests/Fixtures/TemplateFunctions.cs create mode 100644 src/Heddle.Generator.IntegrationTests/ForTests.cs create mode 100644 src/Heddle.Generator.IntegrationTests/FunctionTests.cs create mode 100644 src/Heddle.Generator.IntegrationTests/Heddle.Generator.IntegrationTests.csproj create mode 100644 src/Heddle.Generator.IntegrationTests/ListTests.cs create mode 100644 src/Heddle.Generator.IntegrationTests/NativeExpressionTests.cs create mode 100644 src/Heddle.Generator.IntegrationTests/PartialTests.cs create mode 100644 src/Heddle.Generator.IntegrationTests/PrecompiledSinkTests.cs create mode 100644 src/Heddle.Generator.IntegrationTests/ProfileFlipTests.cs create mode 100644 src/Heddle.Generator.IntegrationTests/PropsTests.cs create mode 100644 src/Heddle.Generator.IntegrationTests/ResolverIntegrationTests.cs create mode 100644 src/Heddle.Generator.IntegrationTests/SlotAndDefaultOutputTests.cs create mode 100644 src/Heddle.Generator.IntegrationTests/StartupProbeTests.cs create mode 100644 src/Heddle.Generator.IntegrationTests/TrimTests.cs create mode 100644 src/Heddle.Generator.IntegrationTests/UnresolvableFunctionTests.cs create mode 100644 src/Heddle.Generator.IntegrationTests/Utf8PieceTests.cs create mode 100644 src/Heddle.Generator.Tests/GeneratorHarness.cs create mode 100644 src/Heddle.Generator.Tests/GeneratorSnapshotTests.cs create mode 100644 src/Heddle.Generator.Tests/Heddle.Generator.Tests.csproj create mode 100644 src/Heddle.Generator.Tests/HeddleGeneratorTests.cs create mode 100644 src/Heddle.Generator.Tests/Snapshots/GeneratorSnapshotTests.Example1_StaticTextAndMemberPaths.verified.txt create mode 100644 src/Heddle.Generator.Tests/Snapshots/GeneratorSnapshotTests.Example2_NativeExpression.verified.txt create mode 100644 src/Heddle.Generator.Tests/Snapshots/GeneratorSnapshotTests.Example3_EmbeddedCSharpVerbatim.verified.txt create mode 100644 src/Heddle.Generator.Tests/Snapshots/GeneratorSnapshotTests.Example4_DynamicTemplate.verified.txt create mode 100644 src/Heddle.Generator.Tests/Snapshots/GeneratorSnapshotTests.Example5_DefinitionWithProps.verified.txt create mode 100644 src/Heddle.Generator.Tests/Snapshots/GeneratorSnapshotTests.Example6_BranchSet.verified.txt create mode 100644 src/Heddle.Generator.Tests/Snapshots/GeneratorSnapshotTests.Example7_FunctionShimCall.verified.txt create mode 100644 src/Heddle.Generator.Tests/Snapshots/GeneratorSnapshotTests.Example7b_UnresolvableFunctionMarker.verified.txt create mode 100644 src/Heddle.Generator/Binding/FunctionExportResolver.cs create mode 100644 src/Heddle.Generator/Binding/SymbolMemberResolver.cs create mode 100644 src/Heddle.Generator/Binding/SymbolTypeResolver.cs create mode 100644 src/Heddle.Generator/Diagnostics/GeneratorDiagnostics.cs create mode 100644 src/Heddle.Generator/Emit/CodeWriter.cs create mode 100644 src/Heddle.Generator/Emit/DocumentShaper.cs create mode 100644 src/Heddle.Generator/Emit/ExtensionBinder.cs create mode 100644 src/Heddle.Generator/Emit/LineMapper.cs create mode 100644 src/Heddle.Generator/Emit/MemberPathWriter.cs create mode 100644 src/Heddle.Generator/Emit/NativeExpressionWriter.cs create mode 100644 src/Heddle.Generator/Emit/PieceWriter.cs create mode 100644 src/Heddle.Generator/Emit/TemplateEmitter.cs create mode 100644 src/Heddle.Generator/Heddle.Generator.csproj create mode 100644 src/Heddle.Generator/HeddleTemplateGenerator.cs create mode 100644 src/Heddle.Generator/Pipeline/ConfigReader.cs create mode 100644 src/Heddle.Generator/Pipeline/GlobalConfig.cs create mode 100644 src/Heddle.Generator/build/Heddle.Generator.props create mode 100644 src/Heddle.Generator/build/Heddle.Generator.targets create mode 100644 src/Heddle.LanguageServer/Heddle.LanguageServer.csproj create mode 100644 src/Heddle.LanguageServer/LspServer.cs create mode 100644 src/Heddle.LanguageServer/Program.cs create mode 100644 src/Heddle.LanguageServer/Protocol/Dtos.cs create mode 100644 src/Heddle.LanguageServer/Protocol/LspJsonContext.cs create mode 100644 src/Heddle.LanguageServer/SemanticTokensBuilder.cs create mode 100644 src/Heddle.LanguageServices.Tests.Corpus/Corpus.cs create mode 100644 src/Heddle.LanguageServices.Tests.Corpus/Heddle.LanguageServices.Tests.Corpus.csproj create mode 100644 src/Heddle.LanguageServices.Tests/AssemblyInfo.cs create mode 100644 src/Heddle.LanguageServices.Tests/Corpus/broken-lib.heddle create mode 100644 src/Heddle.LanguageServices.Tests/Corpus/layout.heddle create mode 100644 src/Heddle.LanguageServices.Tests/Corpus/typo-lib.heddle create mode 100644 src/Heddle.LanguageServices.Tests/CorpusFixture.cs create mode 100644 src/Heddle.LanguageServices.Tests/DemoContractTests.cs create mode 100644 src/Heddle.LanguageServices.Tests/ExportScanTests.cs create mode 100644 src/Heddle.LanguageServices.Tests/Heddle.LanguageServices.Tests.csproj create mode 100644 src/Heddle.LanguageServices.Tests/LanguageServiceCompletionTests.cs create mode 100644 src/Heddle.LanguageServices.Tests/LanguageServiceDefinitionTests.cs create mode 100644 src/Heddle.LanguageServices.Tests/LanguageServiceDiagnosticsTests.cs create mode 100644 src/Heddle.LanguageServices.Tests/LanguageServiceHoverTests.cs create mode 100644 src/Heddle.LanguageServices.Tests/ModelAssemblyReloadTests.cs create mode 100644 src/Heddle.LanguageServices.Tests/PositionMappingTests.cs create mode 100644 src/Heddle.LanguageServices.Tests/ProtocolContractTests.cs create mode 100644 src/Heddle.LanguageServices.Tests/ServerLifecycleTests.cs create mode 100644 src/Heddle.LanguageServices/Completion/CompletionProvider.cs create mode 100644 src/Heddle.LanguageServices/Completion/CompletionText.cs create mode 100644 src/Heddle.LanguageServices/Completion/ContextDetector.cs create mode 100644 src/Heddle.LanguageServices/Completion/DefinitionProvider.cs create mode 100644 src/Heddle.LanguageServices/Completion/HoverProvider.cs create mode 100644 src/Heddle.LanguageServices/CompletionResult.cs create mode 100644 src/Heddle.LanguageServices/DefinitionInfo.cs create mode 100644 src/Heddle.LanguageServices/DocumentAnalysis.cs create mode 100644 src/Heddle.LanguageServices/DocumentAnalyzer.cs create mode 100644 src/Heddle.LanguageServices/ExtensionRegistrar.cs create mode 100644 src/Heddle.LanguageServices/FunctionExportRegistrar.cs create mode 100644 src/Heddle.LanguageServices/Heddle.LanguageServices.csproj create mode 100644 src/Heddle.LanguageServices/HeddleDiagnostic.cs create mode 100644 src/Heddle.LanguageServices/HeddleLanguageService.cs create mode 100644 src/Heddle.LanguageServices/HeddleLanguageServiceOptions.cs create mode 100644 src/Heddle.LanguageServices/ImportLink.cs create mode 100644 src/Heddle.LanguageServices/LineMap.cs create mode 100644 src/Heddle.LanguageServices/ModelAssemblyContext.cs create mode 100644 src/Heddle.LanguageServices/ModelAssemblyManager.cs create mode 100644 src/Heddle.LanguageServices/ScopeMapView.cs create mode 100644 src/Heddle.LanguageServices/WorkspaceConfig.cs create mode 100644 src/Heddle.Performance/BranchRenderBenchmarks.cs create mode 100644 src/Heddle.Performance/LanguageServiceBenchmarks.cs create mode 100644 src/Heddle.Performance/PropsRenderBenchmarks.cs create mode 100644 src/Heddle.Performance/SinkRenderBenchmarks.cs create mode 100644 src/Heddle.Performance/TemplateParseBenchmarks.cs create mode 100644 src/Heddle.Tests/AssemblyInfo.cs create mode 100644 src/Heddle.Tests/BranchAllocationTests.cs create mode 100644 src/Heddle.Tests/BranchConcurrencyTests.cs create mode 100644 src/Heddle.Tests/BranchIsolationTests.cs create mode 100644 src/Heddle.Tests/BranchProtocolTests.cs create mode 100644 src/Heddle.Tests/BranchSetCompilerTests.cs create mode 100644 src/Heddle.Tests/BranchTestExtensions.cs create mode 100644 src/Heddle.Tests/BranchingGoldenTests.cs create mode 100644 src/Heddle.Tests/Data/ErgoForData.cs create mode 100644 src/Heddle.Tests/DefaultFunctionLockstepTests.cs create mode 100644 src/Heddle.Tests/DiagnosticIdTests.cs create mode 100644 src/Heddle.Tests/DoubleEncodeWarningTests.cs create mode 100644 src/Heddle.Tests/DoubleRenderWarningTests.cs create mode 100644 src/Heddle.Tests/ExpressionModeTests.cs create mode 100644 src/Heddle.Tests/FeatureSwitchTests.cs create mode 100644 src/Heddle.Tests/ForSugarAllocationTests.cs create mode 100644 src/Heddle.Tests/ForSugarTests.cs create mode 100644 src/Heddle.Tests/FunctionExportTests.cs create mode 100644 src/Heddle.Tests/FunctionRegistryTests.cs create mode 100644 src/Heddle.Tests/ImportFormsPinningTests.cs create mode 100644 src/Heddle.Tests/ImportOriginTests.cs create mode 100644 src/Heddle.Tests/NativeExpressionConcurrencyTests.cs create mode 100644 src/Heddle.Tests/NativeExpressionGoldenTests.cs create mode 100644 src/Heddle.Tests/NativeExpressionOperatorTests.cs create mode 100644 src/Heddle.Tests/NativeExpressionParseTests.cs create mode 100644 src/Heddle.Tests/NativeExpressionSandboxTests.cs create mode 100644 src/Heddle.Tests/NativeExpressionSmokeTests.cs create mode 100644 src/Heddle.Tests/OutputProfileEncodingTests.cs create mode 100644 src/Heddle.Tests/OutputProfileGoldenTests.cs create mode 100644 src/Heddle.Tests/OutputProfileOptionsTests.cs create mode 100644 src/Heddle.Tests/ParserSettingsSeamTests.cs create mode 100644 src/Heddle.Tests/PrecompiledGauntletTests.cs create mode 100644 src/Heddle.Tests/PrecompiledMismatchPolicyTests.cs create mode 100644 src/Heddle.Tests/PrecompiledRegistryTests.cs create mode 100644 src/Heddle.Tests/PrecompiledRuntimeTests.cs create mode 100644 src/Heddle.Tests/ProfileDirectiveTests.cs create mode 100644 src/Heddle.Tests/PropsBindingTests.cs create mode 100644 src/Heddle.Tests/PropsConcurrencyTests.cs create mode 100644 src/Heddle.Tests/PropsGoldenTests.cs create mode 100644 src/Heddle.Tests/PropsInheritanceTests.cs create mode 100644 src/Heddle.Tests/PropsModels.cs create mode 100644 src/Heddle.Tests/PropsParseTests.cs create mode 100644 src/Heddle.Tests/PropsShadowingTests.cs create mode 100644 src/Heddle.Tests/PublicApiSurfaceTests.cs create mode 100644 src/Heddle.Tests/RangeFunctionTests.cs create mode 100644 src/Heddle.Tests/ScopeChannelDocExampleTests.cs create mode 100644 src/Heddle.Tests/ScopeChannelTests.cs create mode 100644 src/Heddle.Tests/ScopeMapTests.cs create mode 100644 src/Heddle.Tests/SlotProjectionTests.cs create mode 100644 src/Heddle.Tests/Streaming/FormatterSpanGoldenTests.cs create mode 100644 src/Heddle.Tests/Streaming/SinkAdapterTests.cs create mode 100644 src/Heddle.Tests/Streaming/SinkEncodingParityTests.cs create mode 100644 src/Heddle.Tests/Streaming/SinkParityTests.cs create mode 100644 src/Heddle.Tests/Streaming/SinkTestHarness.cs create mode 100644 src/Heddle.Tests/Streaming/SinkTestWriters.cs create mode 100644 src/Heddle.Tests/Streaming/StreamingFixtureAndGuaranteeTests.cs create mode 100644 src/Heddle.Tests/Streaming/Utf8ChunkBoundaryTests.cs create mode 100644 src/Heddle.Tests/TemplateKeyNormalizationTests.cs create mode 100644 src/Heddle.Tests/TemplateOptionsCompletenessTests.cs create mode 100644 src/Heddle.Tests/TemplateResolverProfileTests.cs create mode 100644 src/Heddle.Tests/TestTemplate/branch-import-def.heddle create mode 100644 src/Heddle.Tests/TestTemplate/branch-import-else.heddle create mode 100644 src/Heddle.Tests/TestTemplate/branching-flagship.heddle create mode 100644 src/Heddle.Tests/TestTemplate/branching-interleaved.heddle create mode 100644 src/Heddle.Tests/TestTemplate/branching-list-alternating.heddle create mode 100644 src/Heddle.Tests/TestTemplate/branching-nested.heddle create mode 100644 src/Heddle.Tests/TestTemplate/branching-out-projection.heddle create mode 100644 src/Heddle.Tests/TestTemplate/branching-partial-child.heddle create mode 100644 src/Heddle.Tests/TestTemplate/branching-partial-parent.heddle create mode 100644 src/Heddle.Tests/TestTemplate/ergo-double-render.heddle create mode 100644 src/Heddle.Tests/TestTemplate/ergo-for.heddle create mode 100644 src/Heddle.Tests/TestTemplate/ergo-import-broken.heddle create mode 100644 src/Heddle.Tests/TestTemplate/ergo-import-composition.heddle create mode 100644 src/Heddle.Tests/TestTemplate/ergo-import-empty.heddle create mode 100644 src/Heddle.Tests/TestTemplate/ergo-import-inline.heddle create mode 100644 src/Heddle.Tests/TestTemplate/ergo-import-library.heddle create mode 100644 src/Heddle.Tests/TestTemplate/ergo-trim-preamble.heddle create mode 100644 src/Heddle.Tests/TestTemplate/expr-flagship.heddle create mode 100644 src/Heddle.Tests/TestTemplate/expr-functions.heddle create mode 100644 src/Heddle.Tests/TestTemplate/generated-branching-flagship-archived.html create mode 100644 src/Heddle.Tests/TestTemplate/generated-branching-flagship-featured.html create mode 100644 src/Heddle.Tests/TestTemplate/generated-branching-flagship-regular.html create mode 100644 src/Heddle.Tests/TestTemplate/generated-branching-list-alternating.html create mode 100644 src/Heddle.Tests/TestTemplate/generated-branching-nested.html create mode 100644 src/Heddle.Tests/TestTemplate/generated-branching-out-projection.html create mode 100644 src/Heddle.Tests/TestTemplate/generated-branching-partial.html create mode 100644 src/Heddle.Tests/TestTemplate/generated-ergo-double-render.html create mode 100644 src/Heddle.Tests/TestTemplate/generated-ergo-for.html create mode 100644 src/Heddle.Tests/TestTemplate/generated-ergo-import-composition.html create mode 100644 src/Heddle.Tests/TestTemplate/generated-ergo-trim-off.html create mode 100644 src/Heddle.Tests/TestTemplate/generated-ergo-trim-on.html create mode 100644 src/Heddle.Tests/TestTemplate/generated-expr-flagship.html create mode 100644 src/Heddle.Tests/TestTemplate/generated-expr-functions.html create mode 100644 src/Heddle.Tests/TestTemplate/generated-profile-directive.html create mode 100644 src/Heddle.Tests/TestTemplate/generated-profile-flagship-html.html create mode 100644 src/Heddle.Tests/TestTemplate/generated-profile-flagship-text.html create mode 100644 src/Heddle.Tests/TestTemplate/generated-profile-partial.html create mode 100644 src/Heddle.Tests/TestTemplate/generated-props-abstract-panel.html create mode 100644 src/Heddle.Tests/TestTemplate/generated-props-card.html create mode 100644 src/Heddle.Tests/TestTemplate/generated-props-defaults.html create mode 100644 src/Heddle.Tests/TestTemplate/generated-props-inherit.html create mode 100644 src/Heddle.Tests/TestTemplate/generated-slot-compose.html create mode 100644 src/Heddle.Tests/TestTemplate/generated-slot-picker.html create mode 100644 src/Heddle.Tests/TestTemplate/generated-streaming-unicode.html create mode 100644 src/Heddle.Tests/TestTemplate/import-origin-a.heddle create mode 100644 src/Heddle.Tests/TestTemplate/import-origin-b.heddle create mode 100644 src/Heddle.Tests/TestTemplate/import-origin-badmember-lib.heddle create mode 100644 src/Heddle.Tests/TestTemplate/import-origin-broken.heddle create mode 100644 src/Heddle.Tests/TestTemplate/import-origin-c.heddle create mode 100644 src/Heddle.Tests/TestTemplate/profile-directive.heddle create mode 100644 src/Heddle.Tests/TestTemplate/profile-flagship.heddle create mode 100644 src/Heddle.Tests/TestTemplate/profile-partial-child.heddle create mode 100644 src/Heddle.Tests/TestTemplate/profile-partial-parent.heddle create mode 100644 src/Heddle.Tests/TestTemplate/profile-resolver-default.heddle create mode 100644 src/Heddle.Tests/TestTemplate/props-abstract-panel.heddle create mode 100644 src/Heddle.Tests/TestTemplate/props-card.heddle create mode 100644 src/Heddle.Tests/TestTemplate/props-defaults.heddle create mode 100644 src/Heddle.Tests/TestTemplate/props-inherit.heddle create mode 100644 src/Heddle.Tests/TestTemplate/public-api-heddle-language.txt create mode 100644 src/Heddle.Tests/TestTemplate/public-api-heddle.txt create mode 100644 src/Heddle.Tests/TestTemplate/slot-compose.heddle create mode 100644 src/Heddle.Tests/TestTemplate/slot-picker.heddle create mode 100644 src/Heddle.Tests/TestTemplate/streaming-large.heddle create mode 100644 src/Heddle.Tests/TestTemplate/streaming-unicode.heddle create mode 100644 src/Heddle.Tests/TrimDirectiveLinesTests.cs create mode 100644 src/Heddle.Tool.Tests/Heddle.Tool.Tests.csproj create mode 100644 src/Heddle.Tool.Tests/HeddleToolTests.cs create mode 100644 src/Heddle.Tool/Heddle.Tool.csproj create mode 100644 src/Heddle.Tool/HeddleRenderer.cs create mode 100644 src/Heddle.Tool/Program.cs create mode 100644 src/Heddle/Attributes/ExportFunctionsAttribute.cs create mode 100644 src/Heddle/Attributes/ScopeChannelAttribute.cs create mode 100644 src/Heddle/Core/SlotContent.cs create mode 100644 src/Heddle/Data/BranchState.cs create mode 100644 src/Heddle/Data/ExpressionMode.cs create mode 100644 src/Heddle/Data/HeddleDiagnosticIds.cs create mode 100644 src/Heddle/Data/ISpanScopeRenderer.cs create mode 100644 src/Heddle/Data/IUtf8ScopeRenderer.cs create mode 100644 src/Heddle/Data/LinePosition.cs create mode 100644 src/Heddle/Data/OutputProfile.cs create mode 100644 src/Heddle/Data/PrecompiledMismatchPolicy.cs create mode 100644 src/Heddle/Data/ScopeLocals.cs create mode 100644 src/Heddle/Data/ScopeRendererExtensions.cs create mode 100644 src/Heddle/Data/TextWriterScopeRenderer.cs create mode 100644 src/Heddle/Data/Utf8ScopeRenderer.cs create mode 100644 src/Heddle/Extensions/BranchCondition.cs create mode 100644 src/Heddle/Extensions/ElifExtension.cs create mode 100644 src/Heddle/Extensions/ElseExtension.cs create mode 100644 src/Heddle/Extensions/ProfileExtension.cs create mode 100644 src/Heddle/ILLink.Substitutions.xml create mode 100644 src/Heddle/Language/DocumentParser.Runtime.cs create mode 100644 src/Heddle/Language/Expressions/ExprNode.cs create mode 100644 src/Heddle/Language/Expressions/ExprOperator.cs create mode 100644 src/Heddle/Language/Expressions/ExpressionAstBuilder.cs create mode 100644 src/Heddle/Language/Expressions/ThisNode.cs create mode 100644 src/Heddle/Language/ImportOrigin.cs create mode 100644 src/Heddle/Language/NamedArgument.cs create mode 100644 src/Heddle/Language/ParserSettings.cs create mode 100644 src/Heddle/Language/PropDeclaration.cs create mode 100644 src/Heddle/Native/RoslynReferenceProvider.cs create mode 100644 src/Heddle/Precompiled/DefaultFunctionTable.cs create mode 100644 src/Heddle/Precompiled/HeddleCompiledTemplatesAttribute.cs create mode 100644 src/Heddle/Precompiled/IHeddleTemplateManifest.cs create mode 100644 src/Heddle/Precompiled/PrecompiledCapabilities.cs create mode 100644 src/Heddle/Precompiled/PrecompiledExtensionBinding.cs create mode 100644 src/Heddle/Precompiled/PrecompiledFallbackEvent.cs create mode 100644 src/Heddle/Precompiled/PrecompiledFallbackReason.cs create mode 100644 src/Heddle/Precompiled/PrecompiledFunctionBinding.cs create mode 100644 src/Heddle/Precompiled/PrecompiledFunctions.cs create mode 100644 src/Heddle/Precompiled/PrecompiledGauntlet.cs create mode 100644 src/Heddle/Precompiled/PrecompiledImport.cs create mode 100644 src/Heddle/Precompiled/PrecompiledMismatchException.cs create mode 100644 src/Heddle/Precompiled/PrecompiledOptionsFingerprint.cs create mode 100644 src/Heddle/Precompiled/PrecompiledPropSetter.cs create mode 100644 src/Heddle/Precompiled/PrecompiledRegistrationException.cs create mode 100644 src/Heddle/Precompiled/PrecompiledRuntime.cs create mode 100644 src/Heddle/Precompiled/PrecompiledTemplateInfo.cs create mode 100644 src/Heddle/Precompiled/PrecompiledTemplates.cs create mode 100644 src/Heddle/Precompiled/TemplateKey.cs create mode 100644 src/Heddle/Runtime/Expressions/BuiltInFunctions.cs create mode 100644 src/Heddle/Runtime/Expressions/FunctionEntry.cs create mode 100644 src/Heddle/Runtime/Expressions/FunctionRegistry.cs create mode 100644 src/Heddle/Runtime/Expressions/MemberPathResolver.cs create mode 100644 src/Heddle/Runtime/Expressions/NativeExpressionCompiler.cs create mode 100644 src/Heddle/Runtime/Expressions/NumericPromotion.cs create mode 100644 src/Heddle/Runtime/Expressions/PropConversion.cs create mode 100644 src/Heddle/Runtime/Expressions/PropLayout.cs create mode 100644 src/Heddle/Runtime/HeddleFeatures.cs create mode 100644 src/Heddle/Runtime/Parameters/PropsCompiledParameter.cs create mode 100644 src/Heddle/Runtime/Parameters/PropsSlotParameter.cs create mode 100644 src/Heddle/Runtime/PropsBinder.cs create mode 100644 src/Heddle/Runtime/ScopeMap.cs diff --git a/.gitattributes b/.gitattributes index 412eeda7..df02c549 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,6 +1,17 @@ # Auto detect text files and perform LF normalization * text=auto +# Template fixtures are byte-significant for golden comparisons: their raw newlines flow +# into rendered output. Pin them to LF on every platform so Windows checkouts don't smudge +# them to CRLF and break fixture-derived golden tests (goldens are read with \r\n -> \n). +*.heddle text eol=lf + +# Golden output fixtures are compared verbatim (raw Assert.Equal, no \r\n normalization) against +# rendered output, which is LF because the .heddle templates above are pinned LF. Pin the goldens +# to LF too so a Windows checkout doesn't smudge them to CRLF while the templates stay LF — +# otherwise expected(CRLF) != actual(LF) fails on Windows CI while Linux (LF everywhere) passes. +src/Heddle.Tests/TestTemplate/*.html text eol=lf + # Custom for Visual Studio *.cs diff=csharp *.sln merge=union diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index e15cce55..cd6c0477 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -1,5 +1,6 @@ -# Builds the VitePress documentation site from docs/*.md and deploys it to GitHub +# Builds the VitePress documentation site (incl. the WASM demo bundle) and deploys it to GitHub # Pages. One-time setup: repo Settings -> Pages -> Source = "GitHub Actions". +# PRs build + smoke-test the site (incl. the demo) but never deploy; only main deploys. name: Heddle Docs @@ -9,9 +10,23 @@ on: paths: - 'docs/**' - '.github/workflows/docs.yml' - # The live demo embeds the Ace editor bundle, built from these sources. - - 'src/Heddle.Language/js/**' - - 'src/Heddle.Language/build_ace.sh' + # The demo WASM bundle embeds the engine + facade (build_ace.sh and js/** are covered by the + # whole-project globs below, which supersede the old entries): + - 'src/Heddle/**' + - 'src/Heddle.Language/**' + - 'src/Heddle.LanguageServices/**' + - 'src/Heddle.Demo.Models/**' + - 'src/Heddle.Demo.Wasm/**' + pull_request: + branches: [ "main" ] + paths: # identical list, repeated verbatim — GitHub Actions + - 'docs/**' # path filters are per-trigger; keep the two lists + - '.github/workflows/docs.yml' # in sync when either changes + - 'src/Heddle/**' + - 'src/Heddle.Language/**' + - 'src/Heddle.LanguageServices/**' + - 'src/Heddle.Demo.Models/**' + - 'src/Heddle.Demo.Wasm/**' workflow_dispatch: permissions: @@ -39,9 +54,8 @@ jobs: cache: npm cache-dependency-path: docs/package-lock.json # Build the custom Ace editor bundle (Ace + Heddle mode + ANTLR JS parser) and stage it - # into docs/public so VitePress copies it to the site root. Serves the demo at - # /Heddle/demo.html with the worker loaded same-origin from /Heddle/ace/. No Java needed - # (the JS parser under js/ is already generated; the script only assembles the bundle). + # into docs/public so VitePress copies it to the site root. No Java needed (the JS parser + # under js/ is already generated; the script only assembles the bundle). - name: Build Ace editor bundle working-directory: src/Heddle.Language run: bash build_ace.sh @@ -49,21 +63,52 @@ jobs: run: | mkdir -p docs/public/ace cp -R src/Heddle.Language/ace_build/ace/build/src-noconflict/. docs/public/ace/ + # ---- Phase 9: the typed WASM demo bundle (module worker + _framework), Roslyn-free ---- + - name: Setup .NET + uses: actions/setup-dotnet@v5 + with: + dotnet-version: 10.0.x + - name: Install wasm-tools workload + run: dotnet workload install wasm-tools + - name: Publish demo WASM host + run: dotnet publish src/Heddle.Demo.Wasm -c Release + - name: Stage demo bundle into docs/public + run: | + mkdir -p docs/public/demo + cp -R src/Heddle.Demo.Wasm/bin/Release/net10.0/publish/wwwroot/. docs/public/demo/ + - name: Demo bundle budget and purity gate + run: | + size=$(du -sb docs/public/demo | cut -f1) + echo "demo bundle: $size bytes" + test "$size" -le 12582912 || { echo "::error::demo bundle exceeds 12 MB budget"; exit 1; } + if find docs/public/demo -name 'Microsoft.CodeAnalysis*' | grep -q .; then + echo "::error::Roslyn assets found in the demo bundle (D4 regression)"; exit 1 + fi - name: Install dependencies working-directory: docs run: npm ci - name: Build site working-directory: docs run: npm run docs:build + # ---- Phase 9: the demo smoke suite (chromium only) against the built site ---- + - name: Install Playwright chromium + working-directory: docs + run: npx playwright install chromium --with-deps + - name: Demo smoke tests + working-directory: docs + run: npm run demo:smoke - name: Configure Pages + if: github.event_name != 'pull_request' uses: actions/configure-pages@v6 - name: Upload artifact + if: github.event_name != 'pull_request' uses: actions/upload-pages-artifact@v5 with: path: docs/.vitepress/dist deploy: needs: build + if: github.event_name != 'pull_request' runs-on: ubuntu-latest environment: name: github-pages diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index 82b24230..a82982d0 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -60,10 +60,12 @@ jobs: 10.0.x - name: Restore dependencies run: dotnet restore - - name: Build - run: dotnet build --no-restore -c Release + # No `dotnet build --no-build` split here: Heddle.Tool / Heddle.LanguageServer are RID-specific + # .NET tools (PackAsTool + multiple RuntimeIdentifiers), so `dotnet pack` must publish each RID. + # A RID-less `dotnet build` never produces those per-RID outputs, so `--no-build` fails with + # MSB3030 (missing per-RID assemblies) / NETSDK1085. Let pack build+publish, like the release job. - name: Pack beta packages - run: dotnet pack -c Release --no-build -o ${{ runner.temp }}/pkg --version-suffix "-beta.${{ github.run_number }}" -p:IncludeSymbols=true -p:SymbolPackageFormat=snupkg + run: dotnet pack -c Release -o ${{ runner.temp }}/pkg --version-suffix "-beta.${{ github.run_number }}" - name: NuGet login (OIDC -> short-lived API key) uses: NuGet/login@v1 id: nuget-login @@ -96,7 +98,7 @@ jobs: - name: Restore dependencies run: dotnet restore - name: Pack release packages - run: dotnet pack -c Release -p:Version=${{ steps.ver.outputs.version }} -o ${{ runner.temp }}/pkg -p:IncludeSymbols=true -p:SymbolPackageFormat=snupkg + run: dotnet pack -c Release -p:Version=${{ steps.ver.outputs.version }} -o ${{ runner.temp }}/pkg - name: NuGet login (OIDC -> short-lived API key) uses: NuGet/login@v1 id: nuget-login diff --git a/.github/workflows/lsp.yml b/.github/workflows/lsp.yml new file mode 100644 index 00000000..f6bd8804 --- /dev/null +++ b/.github/workflows/lsp.yml @@ -0,0 +1,95 @@ +# Builds and tests the Heddle language services (facade + LSP server), packs the version-pinned +# per-RID dotnet tool, and packages the VS Code extension. Phase 6 (editor tooling / LSP). + +name: Heddle LSP + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + +permissions: + contents: read + +jobs: + test: + runs-on: windows-latest + steps: + - uses: actions/checkout@v7 + - uses: actions/setup-dotnet@v4 + with: + # The multi-targeted test suite runs net6.0/net8.0/net10.0/net48; the runner image + # no longer ships the EOL .NET 6 (and may drop 8) runtime, so install them explicitly + # alongside the global.json SDK — otherwise the net6.0 test host aborts (NETCore.App 6 missing). + dotnet-version: | + 6.0.x + 8.0.x + global-json-file: global.json + - name: Build (Release) + run: dotnet build -c Release Heddle.sln + - name: Test engine suite + run: dotnet test src/Heddle.Tests/Heddle.Tests.csproj -c Release --no-build + - name: Test language services + run: dotnet test src/Heddle.LanguageServices.Tests/Heddle.LanguageServices.Tests.csproj -c Release --no-build + + pack-tool: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - uses: actions/setup-dotnet@v4 + with: + global-json-file: global.json + # Framework-dependent + R2R per-RID packs cross-pack from one runner (phase 6 D19). + # -p:EnableToolR2R=true makes PublishReadyToRun visible at the RID-less outer restore so + # the crossgen2 runtime packs for every RuntimeIdentifier are restored (else NETSDK1094). + - name: Pack heddle-lsp (manifest + per-RID + any) + run: dotnet pack src/Heddle.LanguageServer/Heddle.LanguageServer.csproj -c Release -o ./nupkg -p:EnableToolR2R=true + - name: Smoke — install and run --version + run: | + set -euo pipefail + dotnet tool install --global Heddle.LanguageServer --add-source ./nupkg --version 1.0.0 || true + export PATH="$PATH:$HOME/.dotnet/tools" + heddle-lsp --version + - uses: actions/upload-artifact@v4 + with: + name: heddle-lsp-nupkg + path: ./nupkg/*.nupkg + + vsix: + runs-on: ubuntu-latest + strategy: + matrix: + target: [win32-x64, win32-arm64, linux-x64, linux-arm64, alpine-x64, darwin-x64, darwin-arm64] + steps: + - uses: actions/checkout@v7 + - uses: actions/setup-dotnet@v4 + with: + global-json-file: global.json + - uses: actions/setup-node@v4 + with: + node-version: '20' + - name: Map VS Code target to .NET RID + id: rid + run: | + case "${{ matrix.target }}" in + win32-x64) echo "rid=win-x64" >> "$GITHUB_OUTPUT" ;; + win32-arm64) echo "rid=win-arm64" >> "$GITHUB_OUTPUT" ;; + linux-x64) echo "rid=linux-x64" >> "$GITHUB_OUTPUT" ;; + linux-arm64) echo "rid=linux-arm64" >> "$GITHUB_OUTPUT" ;; + alpine-x64) echo "rid=linux-musl-x64" >> "$GITHUB_OUTPUT" ;; + darwin-x64) echo "rid=osx-x64" >> "$GITHUB_OUTPUT" ;; + darwin-arm64) echo "rid=osx-arm64" >> "$GITHUB_OUTPUT" ;; + esac + - name: Publish server (framework-dependent, R2R) + run: dotnet publish src/Heddle.LanguageServer/Heddle.LanguageServer.csproj -c Release -r ${{ steps.rid.outputs.rid }} --self-contained false -o editors/vscode/server -p:EnableToolR2R=true + - name: Package VSIX + working-directory: editors/vscode + run: | + npm ci + npm run copy-grammar + npx @vscode/vsce package --target ${{ matrix.target }} + - uses: actions/upload-artifact@v4 + with: + name: heddle-vsix-${{ matrix.target }} + path: editors/vscode/*.vsix diff --git a/.github/workflows/samples.yml b/.github/workflows/samples.yml new file mode 100644 index 00000000..cd5dca1d --- /dev/null +++ b/.github/workflows/samples.yml @@ -0,0 +1,50 @@ +# Integration demo gallery: every sample is a CI job with golden assertions. +# A red job here is an integration regression in the owning phase's feature. +name: Heddle Samples + +on: + push: + branches: [ "main" ] + paths: + - 'samples/**' + - 'src/**' + - '.github/workflows/samples.yml' + pull_request: + branches: [ "main" ] + paths: # identical list, repeated verbatim — path filters + - 'samples/**' # are per-trigger; keep both lists in sync + - 'src/**' + - '.github/workflows/samples.yml' + +permissions: + contents: read + +jobs: + sample: + strategy: + fail-fast: false # one broken sample must not mask the others + matrix: + sample: + - ssr-aspnetcore + - definition-library + - dynamic-models + - sandboxed-user-templates + - html-safe-output + - custom-extensions + - component-props-slots + - codegen-t4-successor + - precompiled-app + - streaming-ssr + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - name: Setup .NET + uses: actions/setup-dotnet@v5 + with: + dotnet-version: 10.0.x + - name: Comparer self-test + run: bash samples/tools/compare-golden-selftest.sh + - name: Run sample (capture mode) + run: dotnet run --project samples/${{ matrix.sample }} -c Release -- --capture out + - name: Compare against goldens + run: bash samples/tools/compare-golden.sh samples/${{ matrix.sample }} diff --git a/.gitignore b/.gitignore index f2c4efd8..6682af4b 100644 --- a/.gitignore +++ b/.gitignore @@ -119,8 +119,27 @@ project.lock.json /docs/node_modules /docs/.vitepress/dist /docs/.vitepress/cache + +# VS Code extension build artifacts +/editors/vscode/node_modules +/editors/vscode/dist +/editors/vscode/*.vsix +/editors/vscode/server # Ace editor bundle staged into the site by the docs workflow (generated) /docs/public/ace/ +# Phase 9 demo WASM bundle staged into the site by the docs workflow (generated, never committed) +/docs/public/demo/ +# Phase 9 sample gallery capture outputs (written by `dotnet run -- --capture out`; goldens are committed) +samples/**/out/ /.idea /src/Heddle.Language/gen /src/Heddle.Language/.antlr + +# Verify.SourceGenerators snapshot working files +*.received.* +# Phase 9 generator EmitCompilerGeneratedFiles output (build artifact) +samples/**/generated/ +# Playwright demo smoke artifacts +docs/test-results/ +docs/playwright-report/ +/docs/logs diff --git a/Directory.Build.props b/Directory.Build.props index d39d3ed5..39795305 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -16,6 +16,11 @@ README.md template;template-engine;text;codegen;razor-alternative true + + snupkg diff --git a/Heddle.sln b/Heddle.sln index b5591257..d419109d 100644 --- a/Heddle.sln +++ b/Heddle.sln @@ -15,28 +15,218 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Heddle.Performance", "src\H EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Heddle.Tests", "src\Heddle.Tests\Heddle.Tests.csproj", "{EDC55E94-74DF-43AC-A0F9-46B2AD5F0515}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Heddle.LanguageServices", "src\Heddle.LanguageServices\Heddle.LanguageServices.csproj", "{5A84CB76-2835-48BE-BA12-EC96FB483B06}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Heddle.LanguageServices.Tests.Corpus", "src\Heddle.LanguageServices.Tests.Corpus\Heddle.LanguageServices.Tests.Corpus.csproj", "{CE656057-EE58-4ED9-9533-C30B4422F364}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Heddle.LanguageServices.Tests", "src\Heddle.LanguageServices.Tests\Heddle.LanguageServices.Tests.csproj", "{379A11FE-D5DE-428E-A3CA-C4A068CDD802}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Heddle.LanguageServer", "src\Heddle.LanguageServer\Heddle.LanguageServer.csproj", "{AF118206-29D5-4C5A-B0E8-61F2387ADABD}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Heddle.Generator", "src\Heddle.Generator\Heddle.Generator.csproj", "{AA8F1B34-5B40-4F1F-AE5B-73C1490EEB9E}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Heddle.Generator.Tests", "src\Heddle.Generator.Tests\Heddle.Generator.Tests.csproj", "{E44F52CE-3EFB-48F8-9E8C-E30BAF5209F9}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Heddle.Generator.IntegrationTests", "src\Heddle.Generator.IntegrationTests\Heddle.Generator.IntegrationTests.csproj", "{17AA2FAD-265A-4404-832B-B72BB16F8017}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Heddle.Tool", "src\Heddle.Tool\Heddle.Tool.csproj", "{C7BFBA18-1490-4568-AABC-E02F41E4837C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Heddle.Tool.Tests", "src\Heddle.Tool.Tests\Heddle.Tool.Tests.csproj", "{D4D2E547-5D71-4C5A-93F8-F02262322B2C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Heddle.Demo.Models", "src\Heddle.Demo.Models\Heddle.Demo.Models.csproj", "{3704A09C-3CF3-4CB8-BBA5-EDBC6554E13A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Heddle.Demo.Wasm", "src\Heddle.Demo.Wasm\Heddle.Demo.Wasm.csproj", "{81A72F09-FFB8-4967-AB08-141C56A72DE1}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {08216E41-F266-441F-ADB2-D8D7FB088B98}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {08216E41-F266-441F-ADB2-D8D7FB088B98}.Debug|Any CPU.Build.0 = Debug|Any CPU + {08216E41-F266-441F-ADB2-D8D7FB088B98}.Debug|x64.ActiveCfg = Debug|Any CPU + {08216E41-F266-441F-ADB2-D8D7FB088B98}.Debug|x64.Build.0 = Debug|Any CPU + {08216E41-F266-441F-ADB2-D8D7FB088B98}.Debug|x86.ActiveCfg = Debug|Any CPU + {08216E41-F266-441F-ADB2-D8D7FB088B98}.Debug|x86.Build.0 = Debug|Any CPU {08216E41-F266-441F-ADB2-D8D7FB088B98}.Release|Any CPU.ActiveCfg = Release|Any CPU {08216E41-F266-441F-ADB2-D8D7FB088B98}.Release|Any CPU.Build.0 = Release|Any CPU + {08216E41-F266-441F-ADB2-D8D7FB088B98}.Release|x64.ActiveCfg = Release|Any CPU + {08216E41-F266-441F-ADB2-D8D7FB088B98}.Release|x64.Build.0 = Release|Any CPU + {08216E41-F266-441F-ADB2-D8D7FB088B98}.Release|x86.ActiveCfg = Release|Any CPU + {08216E41-F266-441F-ADB2-D8D7FB088B98}.Release|x86.Build.0 = Release|Any CPU {CE96D768-CEF0-414C-8170-1834BC4DB44E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {CE96D768-CEF0-414C-8170-1834BC4DB44E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CE96D768-CEF0-414C-8170-1834BC4DB44E}.Debug|x64.ActiveCfg = Debug|Any CPU + {CE96D768-CEF0-414C-8170-1834BC4DB44E}.Debug|x64.Build.0 = Debug|Any CPU + {CE96D768-CEF0-414C-8170-1834BC4DB44E}.Debug|x86.ActiveCfg = Debug|Any CPU + {CE96D768-CEF0-414C-8170-1834BC4DB44E}.Debug|x86.Build.0 = Debug|Any CPU {CE96D768-CEF0-414C-8170-1834BC4DB44E}.Release|Any CPU.ActiveCfg = Release|Any CPU {CE96D768-CEF0-414C-8170-1834BC4DB44E}.Release|Any CPU.Build.0 = Release|Any CPU + {CE96D768-CEF0-414C-8170-1834BC4DB44E}.Release|x64.ActiveCfg = Release|Any CPU + {CE96D768-CEF0-414C-8170-1834BC4DB44E}.Release|x64.Build.0 = Release|Any CPU + {CE96D768-CEF0-414C-8170-1834BC4DB44E}.Release|x86.ActiveCfg = Release|Any CPU + {CE96D768-CEF0-414C-8170-1834BC4DB44E}.Release|x86.Build.0 = Release|Any CPU {E7E2561E-7D5F-48A1-B5D0-DA4C95FDD7B8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {E7E2561E-7D5F-48A1-B5D0-DA4C95FDD7B8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E7E2561E-7D5F-48A1-B5D0-DA4C95FDD7B8}.Debug|x64.ActiveCfg = Debug|Any CPU + {E7E2561E-7D5F-48A1-B5D0-DA4C95FDD7B8}.Debug|x64.Build.0 = Debug|Any CPU + {E7E2561E-7D5F-48A1-B5D0-DA4C95FDD7B8}.Debug|x86.ActiveCfg = Debug|Any CPU + {E7E2561E-7D5F-48A1-B5D0-DA4C95FDD7B8}.Debug|x86.Build.0 = Debug|Any CPU {E7E2561E-7D5F-48A1-B5D0-DA4C95FDD7B8}.Release|Any CPU.ActiveCfg = Release|Any CPU {E7E2561E-7D5F-48A1-B5D0-DA4C95FDD7B8}.Release|Any CPU.Build.0 = Release|Any CPU + {E7E2561E-7D5F-48A1-B5D0-DA4C95FDD7B8}.Release|x64.ActiveCfg = Release|Any CPU + {E7E2561E-7D5F-48A1-B5D0-DA4C95FDD7B8}.Release|x64.Build.0 = Release|Any CPU + {E7E2561E-7D5F-48A1-B5D0-DA4C95FDD7B8}.Release|x86.ActiveCfg = Release|Any CPU + {E7E2561E-7D5F-48A1-B5D0-DA4C95FDD7B8}.Release|x86.Build.0 = Release|Any CPU {EDC55E94-74DF-43AC-A0F9-46B2AD5F0515}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {EDC55E94-74DF-43AC-A0F9-46B2AD5F0515}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EDC55E94-74DF-43AC-A0F9-46B2AD5F0515}.Debug|x64.ActiveCfg = Debug|Any CPU + {EDC55E94-74DF-43AC-A0F9-46B2AD5F0515}.Debug|x64.Build.0 = Debug|Any CPU + {EDC55E94-74DF-43AC-A0F9-46B2AD5F0515}.Debug|x86.ActiveCfg = Debug|Any CPU + {EDC55E94-74DF-43AC-A0F9-46B2AD5F0515}.Debug|x86.Build.0 = Debug|Any CPU {EDC55E94-74DF-43AC-A0F9-46B2AD5F0515}.Release|Any CPU.ActiveCfg = Release|Any CPU {EDC55E94-74DF-43AC-A0F9-46B2AD5F0515}.Release|Any CPU.Build.0 = Release|Any CPU + {EDC55E94-74DF-43AC-A0F9-46B2AD5F0515}.Release|x64.ActiveCfg = Release|Any CPU + {EDC55E94-74DF-43AC-A0F9-46B2AD5F0515}.Release|x64.Build.0 = Release|Any CPU + {EDC55E94-74DF-43AC-A0F9-46B2AD5F0515}.Release|x86.ActiveCfg = Release|Any CPU + {EDC55E94-74DF-43AC-A0F9-46B2AD5F0515}.Release|x86.Build.0 = Release|Any CPU + {5A84CB76-2835-48BE-BA12-EC96FB483B06}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5A84CB76-2835-48BE-BA12-EC96FB483B06}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5A84CB76-2835-48BE-BA12-EC96FB483B06}.Debug|x64.ActiveCfg = Debug|Any CPU + {5A84CB76-2835-48BE-BA12-EC96FB483B06}.Debug|x64.Build.0 = Debug|Any CPU + {5A84CB76-2835-48BE-BA12-EC96FB483B06}.Debug|x86.ActiveCfg = Debug|Any CPU + {5A84CB76-2835-48BE-BA12-EC96FB483B06}.Debug|x86.Build.0 = Debug|Any CPU + {5A84CB76-2835-48BE-BA12-EC96FB483B06}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5A84CB76-2835-48BE-BA12-EC96FB483B06}.Release|Any CPU.Build.0 = Release|Any CPU + {5A84CB76-2835-48BE-BA12-EC96FB483B06}.Release|x64.ActiveCfg = Release|Any CPU + {5A84CB76-2835-48BE-BA12-EC96FB483B06}.Release|x64.Build.0 = Release|Any CPU + {5A84CB76-2835-48BE-BA12-EC96FB483B06}.Release|x86.ActiveCfg = Release|Any CPU + {5A84CB76-2835-48BE-BA12-EC96FB483B06}.Release|x86.Build.0 = Release|Any CPU + {CE656057-EE58-4ED9-9533-C30B4422F364}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CE656057-EE58-4ED9-9533-C30B4422F364}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CE656057-EE58-4ED9-9533-C30B4422F364}.Debug|x64.ActiveCfg = Debug|Any CPU + {CE656057-EE58-4ED9-9533-C30B4422F364}.Debug|x64.Build.0 = Debug|Any CPU + {CE656057-EE58-4ED9-9533-C30B4422F364}.Debug|x86.ActiveCfg = Debug|Any CPU + {CE656057-EE58-4ED9-9533-C30B4422F364}.Debug|x86.Build.0 = Debug|Any CPU + {CE656057-EE58-4ED9-9533-C30B4422F364}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CE656057-EE58-4ED9-9533-C30B4422F364}.Release|Any CPU.Build.0 = Release|Any CPU + {CE656057-EE58-4ED9-9533-C30B4422F364}.Release|x64.ActiveCfg = Release|Any CPU + {CE656057-EE58-4ED9-9533-C30B4422F364}.Release|x64.Build.0 = Release|Any CPU + {CE656057-EE58-4ED9-9533-C30B4422F364}.Release|x86.ActiveCfg = Release|Any CPU + {CE656057-EE58-4ED9-9533-C30B4422F364}.Release|x86.Build.0 = Release|Any CPU + {379A11FE-D5DE-428E-A3CA-C4A068CDD802}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {379A11FE-D5DE-428E-A3CA-C4A068CDD802}.Debug|Any CPU.Build.0 = Debug|Any CPU + {379A11FE-D5DE-428E-A3CA-C4A068CDD802}.Debug|x64.ActiveCfg = Debug|Any CPU + {379A11FE-D5DE-428E-A3CA-C4A068CDD802}.Debug|x64.Build.0 = Debug|Any CPU + {379A11FE-D5DE-428E-A3CA-C4A068CDD802}.Debug|x86.ActiveCfg = Debug|Any CPU + {379A11FE-D5DE-428E-A3CA-C4A068CDD802}.Debug|x86.Build.0 = Debug|Any CPU + {379A11FE-D5DE-428E-A3CA-C4A068CDD802}.Release|Any CPU.ActiveCfg = Release|Any CPU + {379A11FE-D5DE-428E-A3CA-C4A068CDD802}.Release|Any CPU.Build.0 = Release|Any CPU + {379A11FE-D5DE-428E-A3CA-C4A068CDD802}.Release|x64.ActiveCfg = Release|Any CPU + {379A11FE-D5DE-428E-A3CA-C4A068CDD802}.Release|x64.Build.0 = Release|Any CPU + {379A11FE-D5DE-428E-A3CA-C4A068CDD802}.Release|x86.ActiveCfg = Release|Any CPU + {379A11FE-D5DE-428E-A3CA-C4A068CDD802}.Release|x86.Build.0 = Release|Any CPU + {AF118206-29D5-4C5A-B0E8-61F2387ADABD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AF118206-29D5-4C5A-B0E8-61F2387ADABD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AF118206-29D5-4C5A-B0E8-61F2387ADABD}.Debug|x64.ActiveCfg = Debug|Any CPU + {AF118206-29D5-4C5A-B0E8-61F2387ADABD}.Debug|x64.Build.0 = Debug|Any CPU + {AF118206-29D5-4C5A-B0E8-61F2387ADABD}.Debug|x86.ActiveCfg = Debug|Any CPU + {AF118206-29D5-4C5A-B0E8-61F2387ADABD}.Debug|x86.Build.0 = Debug|Any CPU + {AF118206-29D5-4C5A-B0E8-61F2387ADABD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AF118206-29D5-4C5A-B0E8-61F2387ADABD}.Release|Any CPU.Build.0 = Release|Any CPU + {AF118206-29D5-4C5A-B0E8-61F2387ADABD}.Release|x64.ActiveCfg = Release|Any CPU + {AF118206-29D5-4C5A-B0E8-61F2387ADABD}.Release|x64.Build.0 = Release|Any CPU + {AF118206-29D5-4C5A-B0E8-61F2387ADABD}.Release|x86.ActiveCfg = Release|Any CPU + {AF118206-29D5-4C5A-B0E8-61F2387ADABD}.Release|x86.Build.0 = Release|Any CPU + {AA8F1B34-5B40-4F1F-AE5B-73C1490EEB9E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AA8F1B34-5B40-4F1F-AE5B-73C1490EEB9E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AA8F1B34-5B40-4F1F-AE5B-73C1490EEB9E}.Debug|x64.ActiveCfg = Debug|Any CPU + {AA8F1B34-5B40-4F1F-AE5B-73C1490EEB9E}.Debug|x64.Build.0 = Debug|Any CPU + {AA8F1B34-5B40-4F1F-AE5B-73C1490EEB9E}.Debug|x86.ActiveCfg = Debug|Any CPU + {AA8F1B34-5B40-4F1F-AE5B-73C1490EEB9E}.Debug|x86.Build.0 = Debug|Any CPU + {AA8F1B34-5B40-4F1F-AE5B-73C1490EEB9E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AA8F1B34-5B40-4F1F-AE5B-73C1490EEB9E}.Release|Any CPU.Build.0 = Release|Any CPU + {AA8F1B34-5B40-4F1F-AE5B-73C1490EEB9E}.Release|x64.ActiveCfg = Release|Any CPU + {AA8F1B34-5B40-4F1F-AE5B-73C1490EEB9E}.Release|x64.Build.0 = Release|Any CPU + {AA8F1B34-5B40-4F1F-AE5B-73C1490EEB9E}.Release|x86.ActiveCfg = Release|Any CPU + {AA8F1B34-5B40-4F1F-AE5B-73C1490EEB9E}.Release|x86.Build.0 = Release|Any CPU + {E44F52CE-3EFB-48F8-9E8C-E30BAF5209F9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E44F52CE-3EFB-48F8-9E8C-E30BAF5209F9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E44F52CE-3EFB-48F8-9E8C-E30BAF5209F9}.Debug|x64.ActiveCfg = Debug|Any CPU + {E44F52CE-3EFB-48F8-9E8C-E30BAF5209F9}.Debug|x64.Build.0 = Debug|Any CPU + {E44F52CE-3EFB-48F8-9E8C-E30BAF5209F9}.Debug|x86.ActiveCfg = Debug|Any CPU + {E44F52CE-3EFB-48F8-9E8C-E30BAF5209F9}.Debug|x86.Build.0 = Debug|Any CPU + {E44F52CE-3EFB-48F8-9E8C-E30BAF5209F9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E44F52CE-3EFB-48F8-9E8C-E30BAF5209F9}.Release|Any CPU.Build.0 = Release|Any CPU + {E44F52CE-3EFB-48F8-9E8C-E30BAF5209F9}.Release|x64.ActiveCfg = Release|Any CPU + {E44F52CE-3EFB-48F8-9E8C-E30BAF5209F9}.Release|x64.Build.0 = Release|Any CPU + {E44F52CE-3EFB-48F8-9E8C-E30BAF5209F9}.Release|x86.ActiveCfg = Release|Any CPU + {E44F52CE-3EFB-48F8-9E8C-E30BAF5209F9}.Release|x86.Build.0 = Release|Any CPU + {17AA2FAD-265A-4404-832B-B72BB16F8017}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {17AA2FAD-265A-4404-832B-B72BB16F8017}.Debug|Any CPU.Build.0 = Debug|Any CPU + {17AA2FAD-265A-4404-832B-B72BB16F8017}.Debug|x64.ActiveCfg = Debug|Any CPU + {17AA2FAD-265A-4404-832B-B72BB16F8017}.Debug|x64.Build.0 = Debug|Any CPU + {17AA2FAD-265A-4404-832B-B72BB16F8017}.Debug|x86.ActiveCfg = Debug|Any CPU + {17AA2FAD-265A-4404-832B-B72BB16F8017}.Debug|x86.Build.0 = Debug|Any CPU + {17AA2FAD-265A-4404-832B-B72BB16F8017}.Release|Any CPU.ActiveCfg = Release|Any CPU + {17AA2FAD-265A-4404-832B-B72BB16F8017}.Release|Any CPU.Build.0 = Release|Any CPU + {17AA2FAD-265A-4404-832B-B72BB16F8017}.Release|x64.ActiveCfg = Release|Any CPU + {17AA2FAD-265A-4404-832B-B72BB16F8017}.Release|x64.Build.0 = Release|Any CPU + {17AA2FAD-265A-4404-832B-B72BB16F8017}.Release|x86.ActiveCfg = Release|Any CPU + {17AA2FAD-265A-4404-832B-B72BB16F8017}.Release|x86.Build.0 = Release|Any CPU + {C7BFBA18-1490-4568-AABC-E02F41E4837C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C7BFBA18-1490-4568-AABC-E02F41E4837C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C7BFBA18-1490-4568-AABC-E02F41E4837C}.Debug|x64.ActiveCfg = Debug|Any CPU + {C7BFBA18-1490-4568-AABC-E02F41E4837C}.Debug|x64.Build.0 = Debug|Any CPU + {C7BFBA18-1490-4568-AABC-E02F41E4837C}.Debug|x86.ActiveCfg = Debug|Any CPU + {C7BFBA18-1490-4568-AABC-E02F41E4837C}.Debug|x86.Build.0 = Debug|Any CPU + {C7BFBA18-1490-4568-AABC-E02F41E4837C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C7BFBA18-1490-4568-AABC-E02F41E4837C}.Release|Any CPU.Build.0 = Release|Any CPU + {C7BFBA18-1490-4568-AABC-E02F41E4837C}.Release|x64.ActiveCfg = Release|Any CPU + {C7BFBA18-1490-4568-AABC-E02F41E4837C}.Release|x64.Build.0 = Release|Any CPU + {C7BFBA18-1490-4568-AABC-E02F41E4837C}.Release|x86.ActiveCfg = Release|Any CPU + {C7BFBA18-1490-4568-AABC-E02F41E4837C}.Release|x86.Build.0 = Release|Any CPU + {D4D2E547-5D71-4C5A-93F8-F02262322B2C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D4D2E547-5D71-4C5A-93F8-F02262322B2C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D4D2E547-5D71-4C5A-93F8-F02262322B2C}.Debug|x64.ActiveCfg = Debug|Any CPU + {D4D2E547-5D71-4C5A-93F8-F02262322B2C}.Debug|x64.Build.0 = Debug|Any CPU + {D4D2E547-5D71-4C5A-93F8-F02262322B2C}.Debug|x86.ActiveCfg = Debug|Any CPU + {D4D2E547-5D71-4C5A-93F8-F02262322B2C}.Debug|x86.Build.0 = Debug|Any CPU + {D4D2E547-5D71-4C5A-93F8-F02262322B2C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D4D2E547-5D71-4C5A-93F8-F02262322B2C}.Release|Any CPU.Build.0 = Release|Any CPU + {D4D2E547-5D71-4C5A-93F8-F02262322B2C}.Release|x64.ActiveCfg = Release|Any CPU + {D4D2E547-5D71-4C5A-93F8-F02262322B2C}.Release|x64.Build.0 = Release|Any CPU + {D4D2E547-5D71-4C5A-93F8-F02262322B2C}.Release|x86.ActiveCfg = Release|Any CPU + {D4D2E547-5D71-4C5A-93F8-F02262322B2C}.Release|x86.Build.0 = Release|Any CPU + {3704A09C-3CF3-4CB8-BBA5-EDBC6554E13A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3704A09C-3CF3-4CB8-BBA5-EDBC6554E13A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3704A09C-3CF3-4CB8-BBA5-EDBC6554E13A}.Debug|x64.ActiveCfg = Debug|Any CPU + {3704A09C-3CF3-4CB8-BBA5-EDBC6554E13A}.Debug|x64.Build.0 = Debug|Any CPU + {3704A09C-3CF3-4CB8-BBA5-EDBC6554E13A}.Debug|x86.ActiveCfg = Debug|Any CPU + {3704A09C-3CF3-4CB8-BBA5-EDBC6554E13A}.Debug|x86.Build.0 = Debug|Any CPU + {3704A09C-3CF3-4CB8-BBA5-EDBC6554E13A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3704A09C-3CF3-4CB8-BBA5-EDBC6554E13A}.Release|Any CPU.Build.0 = Release|Any CPU + {3704A09C-3CF3-4CB8-BBA5-EDBC6554E13A}.Release|x64.ActiveCfg = Release|Any CPU + {3704A09C-3CF3-4CB8-BBA5-EDBC6554E13A}.Release|x64.Build.0 = Release|Any CPU + {3704A09C-3CF3-4CB8-BBA5-EDBC6554E13A}.Release|x86.ActiveCfg = Release|Any CPU + {3704A09C-3CF3-4CB8-BBA5-EDBC6554E13A}.Release|x86.Build.0 = Release|Any CPU + {81A72F09-FFB8-4967-AB08-141C56A72DE1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {81A72F09-FFB8-4967-AB08-141C56A72DE1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {81A72F09-FFB8-4967-AB08-141C56A72DE1}.Debug|x64.ActiveCfg = Debug|Any CPU + {81A72F09-FFB8-4967-AB08-141C56A72DE1}.Debug|x64.Build.0 = Debug|Any CPU + {81A72F09-FFB8-4967-AB08-141C56A72DE1}.Debug|x86.ActiveCfg = Debug|Any CPU + {81A72F09-FFB8-4967-AB08-141C56A72DE1}.Debug|x86.Build.0 = Debug|Any CPU + {81A72F09-FFB8-4967-AB08-141C56A72DE1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {81A72F09-FFB8-4967-AB08-141C56A72DE1}.Release|Any CPU.Build.0 = Release|Any CPU + {81A72F09-FFB8-4967-AB08-141C56A72DE1}.Release|x64.ActiveCfg = Release|Any CPU + {81A72F09-FFB8-4967-AB08-141C56A72DE1}.Release|x64.Build.0 = Release|Any CPU + {81A72F09-FFB8-4967-AB08-141C56A72DE1}.Release|x86.ActiveCfg = Release|Any CPU + {81A72F09-FFB8-4967-AB08-141C56A72DE1}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -46,6 +236,17 @@ Global {CE96D768-CEF0-414C-8170-1834BC4DB44E} = {7A42C497-6F07-4B5E-9B4A-807D444A5277} {E7E2561E-7D5F-48A1-B5D0-DA4C95FDD7B8} = {7A42C497-6F07-4B5E-9B4A-807D444A5277} {EDC55E94-74DF-43AC-A0F9-46B2AD5F0515} = {7A42C497-6F07-4B5E-9B4A-807D444A5277} + {5A84CB76-2835-48BE-BA12-EC96FB483B06} = {7A42C497-6F07-4B5E-9B4A-807D444A5277} + {CE656057-EE58-4ED9-9533-C30B4422F364} = {7A42C497-6F07-4B5E-9B4A-807D444A5277} + {379A11FE-D5DE-428E-A3CA-C4A068CDD802} = {7A42C497-6F07-4B5E-9B4A-807D444A5277} + {AF118206-29D5-4C5A-B0E8-61F2387ADABD} = {7A42C497-6F07-4B5E-9B4A-807D444A5277} + {AA8F1B34-5B40-4F1F-AE5B-73C1490EEB9E} = {7A42C497-6F07-4B5E-9B4A-807D444A5277} + {E44F52CE-3EFB-48F8-9E8C-E30BAF5209F9} = {7A42C497-6F07-4B5E-9B4A-807D444A5277} + {17AA2FAD-265A-4404-832B-B72BB16F8017} = {7A42C497-6F07-4B5E-9B4A-807D444A5277} + {C7BFBA18-1490-4568-AABC-E02F41E4837C} = {7A42C497-6F07-4B5E-9B4A-807D444A5277} + {D4D2E547-5D71-4C5A-93F8-F02262322B2C} = {7A42C497-6F07-4B5E-9B4A-807D444A5277} + {3704A09C-3CF3-4CB8-BBA5-EDBC6554E13A} = {7A42C497-6F07-4B5E-9B4A-807D444A5277} + {81A72F09-FFB8-4967-AB08-141C56A72DE1} = {7A42C497-6F07-4B5E-9B4A-807D444A5277} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {D41F54E0-CF7E-433A-9A70-F092AF0654FE} diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts index c9a03d16..39c7624b 100644 --- a/docs/.vitepress/config.mts +++ b/docs/.vitepress/config.mts @@ -42,8 +42,10 @@ export default withMermaid( // existing README stays the single source and links to it still resolve. rewrites: { 'README.md': 'index.md' }, - // assessment.md stays in the repo for contributors but is not published. - srcExclude: ['assessment.md'], + // Contributor material that stays in the repo but is not published to the docs + // site: the assessment, and the evolution roadmap + implementation specs (D9). The + // spec/roadmap set going live is the integration trigger for this exclusion. + srcExclude: ['assessment.md', 'roadmap/**', 'spec/**'], markdown: { languages: [ diff --git a/docs/README.md b/docs/README.md index c4483b73..dd711fd1 100644 --- a/docs/README.md +++ b/docs/README.md @@ -49,7 +49,10 @@ nuances, and **[Built‑in Extensions](built-in-extensions.md)** for the bundled ### I want to *use the engine from C#* (integrators) Read **[Getting Started](getting-started.md)** and the **[C# API Reference](csharp-api.md)** -(`HeddleTemplate`, `TemplateOptions`, `CompileContext`, compile results, file watching). +(`HeddleTemplate`, `TemplateOptions`, `CompileContext`, compile results, file watching). For complete, +runnable integration examples — SSR, definition libraries, dynamic models, sandboxing, safe output, +custom extensions, component libraries, codegen, precompilation, streaming — see the +**[integration sample gallery](../samples/README.md)**. ### I want to *extend the engine* (advanced) Read **[Writing Custom Extensions](custom-extensions.md)** to add your own `@yourhelper(...)` diff --git a/docs/assessment.md b/docs/assessment.md index 4c347f41..eb764e5e 100644 --- a/docs/assessment.md +++ b/docs/assessment.md @@ -1,166 +1,362 @@ -# Language Assessment (candid) +# Language Assessment & Evolution Direction -A frank engineering evaluation of the **Heddle** template language — strengths, weaknesses, and -how it stacks up against the engines people usually weigh it against. This is opinion grounded in -the source, not marketing; the user‑facing docs -([Language Reference](language-reference.md), [Architecture](architecture.md)) are the neutral -reference. +A comprehensive re‑assessment of the **Heddle** template language: what each construct is worth, +what it costs, how the language stands against its competitors inside and outside .NET, and — +the point of this document — which directions are open for evolving it and which one to take. +This replaces the earlier candid assessment; the user‑facing docs +([Language Reference](language-reference.md), [Architecture](architecture.md)) remain the +neutral reference. Everything here is grounded in the source as of v1.0.0. --- ## TL;DR -A well‑engineered, intellectually honest **niche** language with a coherent, distinctive thesis: -*a tiny orthogonal core where control flow is library, plus full compiled & statically‑typed -expressions, plus an inheritance/late‑binding composition model.* It out‑expresses the -logic‑less engines, matches Razor on power while offering a cleaner composition story, and is -measurably faster. Its costs are real and mostly **not** about the engine's quality: high sigil -density, a few missing ergonomic basics, an all‑or‑nothing security model, and a small -ecosystem / bus‑factor. Best for performance‑sensitive, first‑party .NET rendering by a team -that values typed templates and component composition; poor for untrusted templates, polyglot -stacks, or teams wanting batteries‑included tooling. +Heddle is a well‑engineered niche language whose core bet — *a tiny extension‑only grammar, +statically typed per‑use‑site compilation, and decoupled component composition* — is the bet +the rest of the industry is now making outside .NET (Templ in Go, Askama in Rust, HEEx in +Elixir, JSX everywhere). Inside .NET that quadrant is occupied only by Razor/Blazor, which are +welded to ASP.NET Core. **The open ground is framework‑independent, typed, component‑style +templating for .NET** — emails, documents, static sites, code generation, SSR outside MVC. + +The core is worth keeping essentially as‑is. What limits the language is not the thesis but +four concrete debts, in order of strategic severity: + +1. **Raw output by default** — `@(...)` does not HTML‑encode; encoding is opt‑in per extension. +2. **All expression power is welded to Roslyn** (`AllowCSharp`) — which simultaneously blocks a + sandbox, blocks Native AOT, inflates first‑compile cost, and makes even a comparison + require full C#. +3. **Missing everyday ergonomics** — no `else`/`elif`, no named arguments/props, a clunky + counted loop. +4. **Tooling and ecosystem** — no LSP, thin debugging story, single‑author bus factor. + +The recommended direction (§6) is **the typed component engine for .NET**, unlocked by one +keystone investment: a **small native expression language** that removes Roslyn from the +common path. That single move improves security, AOT, startup, and ergonomics at once, and it +is the prerequisite for every other direction worth pursuing. + +--- + +## 1. Identity — what Heddle is + +Mechanically ([Architecture](architecture.md)): + +- A template **compiles to an execution‑ready document** (`RuntimeDocument` / + `IProcessStrategy`) — a tree of extension instances wired to **compiled** value accessors. + Member paths (`@(A.B.C)`) become expression‑tree delegates; embedded C# (`@( @… )`) becomes + Roslyn‑compiled `(model, chained, root)` delegates. No reflection, no re‑parse at render. +- Rendering walks the document and streams through a size‑adaptive buffer. "Compile once, + render many." + +Linguistically, the defining choice is that **the language has almost no built‑in semantics**. +There are no `if`/`for`/`include` keywords — `@if`, `@list`, `@partial` are all *extensions* +(19 ship in the box). The only primitive is a `@`‑prefixed call (optional name, one +parenthesized parameter, optional `{{ }}` body), composed with `:`. The grammar is tiny; the +language grows by adding a class, not by changing the parser. That orthogonality is the spine +everything else hangs from — and, importantly for evolution, **the grammar is fully owned** +(hand‑rolled ANTLR lexer/parser, including the C# token grammar), so surface changes are +constrained only by design taste and back‑compat, not by a third‑party parser. + +--- + +## 2. Feature‑by‑feature valuation + +Each construct valued on both sides. Verdicts: **Keep** (asset, leave alone), **Polish** +(asset with a fixable rough edge), **Evolve** (right idea, needs a design investment), +**Rethink** (liability in its current form). + +### 2.1 Core model + +| Feature | Value | Cost | Verdict | +| --- | --- | --- | --- | +| Extension‑only core (`@name(param){{body}}`) | One uniform primitive; language grows without parser changes; users' directives are peers of built‑ins. | Everything *looks* like a call, so semantics (descend vs. step‑back) are invisible at the call site; discoverability depends on docs. | **Keep** | +| Chain `:` (right‑to‑left composition) | Function composition with typed data flow; wrapper/decorator idiom falls out for free. | Right‑to‑left surprises newcomers; `:` is visually overloaded with `::` and inheritance `:`. | **Keep** (document harder; consider a lint) | +| Chained channel (loop index / pipe value / `@out()` projection) | One mechanism where Razor needs three unrelated features (loop variables, ViewData‑ish side values, `RenderBody`). Compact and typed. | One name for three roles is elegant to the designer and opaque to the reader; `@swap()` is a symptom. | **Keep** | +| Relative context that narrows on descent | Templates read locally (`@(Title)` not `@(Model.Articles[i].Title)`); sections are relocatable, which is what makes composition work. | Mental model tax for Razor emigrants; "what is the model *here*?" is the #1 beginner question. | **Keep** | +| One‑level step‑back for value calls (`@if`, formatters) | Makes the 95% case correct: `@if(flag){{ @(Title) }}` sees the surrounding model. | It is an *invisible* rule — nothing at the call site says whether a call descends or steps back; custom extensions choose freely, so reading unfamiliar templates requires knowing each extension. | **Polish** (surface it in tooling/docs; consider a naming or signature convention for extension authors) | +| `::` root reference | Escape hatch from any depth without `AllowCSharp`; covers app/page context cleanly. | Only two rungs exist — *current* and *root*; anything in between must be threaded by hand (no `..` parent path, deliberately). | **Keep** (the restraint is right; `..` invites fragile coupling) | + +### 2.2 Type system + +| Feature | Value | Cost | Verdict | +| --- | --- | --- | --- | +| Abstract definitions, monomorphised per use site | The language's most distinctive idea: write a section once, each call site compiles it against the concrete model and is independently type‑checked. C++‑template semantics with zero annotation ceremony. Uncommon and genuinely useful. | Errors surface at the *call site* of a definition written elsewhere; without good diagnostics this is confusing. Per‑site compilation multiplies compile work. | **Keep** (invest in diagnostics that name both the definition and the offending use) | +| `:: Type` annotation | Pins a definition; enables member checking, faster accessors, and the inheritance narrowing rule. | Trailing position (after the body) hides the most important fact about a definition at the bottom. | **Polish** (allow the annotation in the header, e.g. ``, keeping the trailing form) | +| `:: dynamic` | Honest escape hatch for genuinely late shapes (Expando, anonymous types across assemblies). | Silently forfeits every compile‑time guarantee; nothing in the template distinguishes a typo from a dynamic miss until render. | **Keep** (it's opt‑in and clearly spelled) | + +### 2.3 Composition + +| Feature | Value | Cost | Verdict | +| --- | --- | --- | --- | +| Definitions + inheritance/override ``, layered in document order | The strongest practical differentiator. Layout and page are *symmetric* — no Razor‑style directionality; any template can base any other; overrides are declarative. Compiles flat, so **composition costs nothing at render time**. | Document‑order override is powerful and slightly spooky (later redefinition changes earlier‑written call sites' *future* renders); no way to "seal" a definition. | **Keep** (consider an opt‑in `sealed` marker later; not urgent) | +| `-> chain` default output | Lets a template of pure definitions render itself; the entry‑layout idiom is clean. | The "don't also call it" double‑render trap; `->` is one more sigil. | **Polish** (compile warning when a `->` definition is also called directly) | +| `@out()` content projection | Slots/`RenderBody` for free via the chained channel; multiple named regions are just multiple definitions — genuinely better than Razor sections. | Single content channel per call; parameterized slots (pass data *into* the projected body) have no story. | **Evolve** (named/parameterized slots are the missing piece for a component ecosystem) | +| Imports `@<<{{ path }}` vs `@partial()` | Clean compile‑time (definitions) vs run‑time (rendered output) split. | Two mechanisms to learn; import syntax `@<<{{ }}` is the strangest‑looking construct in the language. | **Keep** semantics, **Polish** surface (accept `@import(){{ path }}` as the documented spelling; `@<<` stays as the terse form) | +| Recursion (bounded by `MaxRecursionCount`) | Trees render naturally; the bound prevents runaways. | — | **Keep** | + +### 2.4 Expressions + +| Feature | Value | Cost | Verdict | +| --- | --- | --- | --- | +| Member paths → expression‑tree delegates | Fast, null‑safe, statically checked, no Roslyn involved, works with `AllowCSharp = false`. | Paths only — no comparison, arithmetic, indexing, or method call without escalating to full C#. | **Keep** as the floor | +| Embedded C# via Roslyn (full C# 14 expression grammar) | Maximum ceiling — LINQ, lambdas, interpolation; types bind to the real model. The lexer work here is substantial and correct. | The costs compound: (a) **security** — any C# is arbitrary code, so no sandbox is possible; (b) **AOT** — Roslyn emit does not run under Native AOT and expression‑tree `.Compile()` degrades to interpretation, so Heddle is effectively excluded from AOT deployments; (c) **startup** — Roslyn dominates first‑compile cost; (d) **ergonomics** — even `Count > 0` requires `AllowCSharp = true`. | **Keep the ceiling, Evolve the floor** — see §6 keystone | +| *(gap)* No native expression tier | — | Everything between a member path and full C# falls off a cliff. This single gap is what makes the security model all‑or‑nothing and the language Roslyn‑bound. | **Evolve** (the keystone investment, §6) | + +### 2.5 Surface syntax + +| Feature | Value | Cost | Verdict | +| --- | --- | --- | --- | +| `@` as the only special character, `@@` escape | Plain text stays plain; the rule fits in one sentence. | — | **Keep** | +| Sigil inventory (`@% %@`, `{{ }}`, `< >`, `->`, `::`, `:`, `@<<`, `@{ }@`, `@:`, `@\`) | Each is short and, once learned, unambiguous; the lexer modes make them all context‑correct. | The *set* is large and the punctuation overloaded: `:` is chain, base separator, and half of `::`; `{{ }}` is body, import path, and format string. Real templates read densely; this is the #1 first‑impression tax. | **Polish** (don't redesign — add readable aliases where cheap, spend the budget on syntax highlighting/LSP, which dissolves most of the density in practice) | +| Raw blocks `@{ }@` / `@:`, comments‑anywhere, `@\` trimming | All three are exactly right and cheap; comments working mid‑token is a nice ANTLR‑hidden‑channel dividend. | `@\` is needed in preambles because directives don't auto‑trim their own line. | **Polish** (trim the trailing newline of *whole‑line* directives by default under an opt‑in option; HTML output never notices, text/JSON output gets much cleaner) | +| Whitespace‑significant output | Predictable; what you type is what renders. | Preamble noise (mitigated by the above). | **Keep** | + +### 2.6 Output & safety + +| Feature | Value | Cost | Verdict | +| --- | --- | --- | --- | +| Per‑extension encoding (`[EncodeOutput]`), `@(...)` raw / `@html(...)`+`@string(...)` encoded | Flexible; correct for a *text* engine that also emits JSON, code, config. | **The default is unsafe for the dominant use case.** The shortest spelling — `@(Title)` — is the raw one; every modern HTML engine (Razor, Jinja2 autoescape, HEEx, JSX) encodes by default. This is an XSS footgun and the first thing a security review flags. | **Rethink** (2.0: per‑template output profile — `html` profile encodes on `@(...)` with `@raw(...)` opt‑out; `text` profile keeps today's behavior. Breaking, hence the 2.0 window.) | +| `AllowCSharp` all‑or‑nothing | Honest: off = safe, on = full trust. Nothing pretends to be a sandbox. | Off is *too* off (no expressions at all), so real templates turn it on, so effectively all real deployments run untrusted‑unsafe. | **Evolve** (falls out of the native expression tier: off no longer means expressionless) | + +### 2.7 Runtime + +| Feature | Value | Cost | Verdict | +| --- | --- | --- | --- | +| Execution‑ready document, streaming renderer, struct `Scope`, adaptive buffers | Measurably faster than Razor with fewer allocations on the component‑heavy benchmark ([Architecture → Performance](architecture.md#performance-characteristics)); the win is structural (no per‑component activation), so it grows with component density. This is a real, defensible asset. | First compile is expensive (ANTLR + expression trees + Roslyn). | **Keep** (the compile cost is addressed by the build‑time path, §5C) | +| `string Generate(object)` — synchronous, string‑building API | Simple; right for emails/documents/codegen. | No `TextWriter`/`IBufferWriter` sink, no UTF‑8 path, no async data resolution — all three matter for high‑throughput web SSR, which is otherwise Heddle's best benchmark story. | **Evolve** (additive API, no language change) | +| Native AOT / trimming compatibility | — | Roslyn runtime emit: unavailable. Expression trees: interpreter fallback. Reflection‑based member resolution: trim‑hostile. The .NET ecosystem is moving exactly this way. | **Evolve** (source‑generator compile path, §5C) | + +### 2.8 Extensibility & tooling + +| Feature | Value | Cost | Verdict | +| --- | --- | --- | --- | +| Extension authoring model (`InitStart` type negotiation, `ProcessData` + `RenderData`, `Scope` transforms) | Far more powerful than a Liquid filter: extensions participate in type flow and choose their context semantics. | The floor is high: a trivial helper needs a class, an attribute set, two render methods, and an understanding of `ExType`. Liquid's "a filter is a lambda" wins the first hour. | **Polish** (ship a `SimpleExtension` base or a `[TemplateFunction]` lambda registration for the value‑in/string‑out case; keep the full contract for real extensions) | +| Editor assets (Ace mode, TextMate grammar, `ProvideLanguageFeatures` token stream) | Better than most niche languages ship at 1.0; the token stream is the seed of an LSP. | No LSP, no completion/hover/go‑to‑definition; template errors don't map to editor squiggles anywhere. | **Evolve** (an LSP is the single biggest adoption lever after safety) | +| Debuggability | Compile errors are collected, not thrown, with positions. | A broken embedded‑C# expression or a per‑site type failure doesn't yet point at *template line + call site chain* as crisply as Razor's generated‑code mapping; render‑time inspection (step through a template) doesn't exist. | **Evolve** | +| Ecosystem / bus factor | Single coherent vision — the language is *designed*, not accreted. | Single author, .NET‑only, no corpus of answers, sophisticated internals (8‑mode lexer, Roslyn codegen) that few can maintain. Every strategic choice below must be weighed against maintenance capacity. | **Structural constraint** — see §6 sequencing | --- -## What it actually is (mechanics) +## 3. Competitive landscape + +### 3.1 The .NET field + +| | **Heddle** | Razor (MVC/Pages) | Blazor SSR components | Scriban | Fluid (Liquid) | Handlebars.Net | T4 | +| --- | --- | --- | --- | --- | --- | --- | --- | +| Execution | Compiled document | Compiled (Roslyn, whole page) | Compiled | Interpreted (fast) | Interpreted (fast) | Compiled helpers | Build‑time codegen | +| Typing | **Static, per use site** | Static | Static | Dynamic | Dynamic | Dynamic | Static (it *is* C#) | +| Template logic | Paths now; full C# opt‑in | Full C# | Full C# | Rich sandboxed script | Liquid subset, sandboxed | Logic‑less + helpers | Full C# | +| Untrusted templates | No (or expressionless) | No | No | **Yes** | **Yes** | Mostly | No | +| Composition | **Symmetric inheritance + override, free at runtime** | Layouts/sections/partials (directional) | Components + parameters + `RenderFragment` | Includes/functions | Includes | Partials | None to speak of | +| Named parameters/props | **No** | Yes (tag helpers/partials via model) | **Yes (typed props)** | Yes (named args) | Filter args | Hash args | n/a | +| Framework coupling | **None** | ASP.NET Core | ASP.NET Core | None | None | None | VS/MSBuild | +| AOT/trimming | Poor today | Poor (runtime compile) / OK (build‑time) | OK | OK | **Good** | OK | **Good** (build‑time) | +| Ecosystem | Tiny | Enormous | Large | Healthy | Healthy | Healthy | Legacy | + +Readings: -- **Compiles to an execution‑ready document**, not to interpreted templates and not to a single - whole‑template assembly. `HeddleCompiler` builds a `RuntimeDocument`/`IProcessStrategy` — a tree - of extension instances wired to **compiled** value accessors. -- **Member paths** (`@(A.B.C)`) compile to **expression‑tree delegates** - (`System.Linq.Expressions`, `.Compile()`, null‑safe) — reflection only at compile time, never - at render. -- **Embedded C#** (`@( @… )`) compiles via **Roslyn** into `(model, chained, root)` delegates, - which is also where expressions bind to the model's types. Roslyn touches *only* the C# - expressions, not the template structure. -- Rendering walks the document and invokes those delegates — no re‑parse, no reflection per - render. "Compile once, render many." +- **vs Razor/Blazor** — the only .NET engines in Heddle's quadrant (compiled + typed + + components), and both are chained to ASP.NET Core hosting, DI, and tooling. Razor outside a + web app is genuinely unpleasant (RazorLight et al. exist because of it). Heddle's decoupled + composition and per‑component render cost beat Razor's; Razor's tooling, debugging, and + hiring pool beat Heddle's by an order of magnitude. **Don't fight Razor inside MVC; own + everything outside it.** +- **vs Scriban/Fluid** — they own untrusted/user templates and will keep them unless Heddle + ships a sandboxable expression tier; even then they retain the polyglot‑Liquid familiarity. + Their weakness is exactly Heddle's strength: no static typing, no compile‑time member + checking, weaker composition. +- **vs Handlebars.Net/Mustache** — logic‑less philosophy, polyglot reach. Different religion; + not a direct competitor for typed first‑party rendering. +- **vs T4 / source generators** — T4 is the incumbent for build‑time text codegen and it is + unloved (no composition, VS‑bound, ugly). A build‑time Heddle (§5C) is a credible T4 + successor with a *vastly* better language. -## The thesis +### 3.2 Conceptual neighbors outside .NET -The defining choice: **the language has almost no built‑in semantics.** There are no -`if`/`for`/`include` keywords — `@if`, `@list`, `@date`, `@partial` are all *extensions*. The -only primitive is a `@`‑prefixed call (optional name, parenthesized parameter, optional `{{ }}` -body), composed with `:`. The grammar is tiny; the language grows by adding a class, not by -changing the parser. That orthogonality is the spine everything else hangs from. +These matter more for *direction* than competition — they show where typed templating is +converging: + +- **Templ (Go)** — templates compile to Go functions; components are typed functions with + ordinary parameters; LSP ships with the tool. Validates: build‑time compilation + named, + typed props + first‑class tooling. +- **Askama/Maud (Rust)** — templates type‑checked at build time against structs; zero runtime + cost. Validates: the compile‑time direction, and that users accept strictness for safety. +- **HEEx (Elixir/Phoenix)** — compile‑time validated HTML, components with declared + attributes and **named slots**, encode‑by‑default. Validates: slots + safety defaults. +- **JSX/TSX, Svelte** — components won; props are named and typed; composition is the whole + model. Validates: Heddle's composition thesis, and highlights its missing named props. +- **Jinja2/Twig** — the interpreted world's high‑water mark: block inheritance (Heddle's is + better), macros, autoescape‑by‑default (Heddle lacks), sandbox mode (Heddle lacks). + +The industry consensus across ecosystems: **compiled, typed, component‑shaped, safe by +default, with real tooling**. Heddle already has the first three at a level most engines +don't; it is missing the last two. + +### 3.3 Where the open ground is + +Plotting typed↔dynamic against framework‑coupled↔independent: the *typed + independent* +quadrant in .NET is **empty except for Heddle**. Concrete underserved workloads: + +- transactional email / notification rendering (today: Razor‑out‑of‑band hacks or Liquid, + losing typing); +- document/report/export generation; +- code generation and scaffolding (T4's territory); +- static site generation in .NET; +- high‑throughput SSR fragments outside MVC (the benchmark's own story); +- AOT‑deployed services — *nobody* serves typed templates well there yet, including Heddle + today. --- -## Distinctive strengths (the combination, more than any single trait) - -### 1. Extension‑only core -Control flow is a library, so the language is small and uniformly extensible. The `:` chain -composes calls **right‑to‑left** (the leftmost call renders; each receives the right neighbour's -output as its *chained* value, splice‑able via `@out()`). One mechanism — the **chained -channel** — unifies three things other engines keep separate: the **loop index**, the -**pipeline value**, and **content/slot projection** (`@out()`). Razor needs three unrelated -features for that. - -### 2. Abstract, late‑bound definitions (C++ templates, not generics) -A definition with no `:: Type` is *abstract*: it's compiled against the concrete model present -at **each call site** (or pinned when an inheritor narrows it). This is the **C++ template** -model — monomorphised and statically type‑checked *per use*, with no type parameters or -constraints — not C# generics (compiled once behind constraints, abstracted away, not -late‑bound), and not `:: dynamic` (runtime binder). Write a section once; every call site is its -own specialization, each checked. Uncommon and genuinely useful. - -### 3. Composition without coupling -Definition inheritance/override are *declarative extension points*, not forward dependencies. -Unlike Razor sections — which bind "backwards," pinning the rendered page as the layout's final -consumer — a Heddle page can be split into independent templates recombined by a layout, any page -can serve as a base for another, and (because it all compiles to one flat document) -**composition costs nothing at render time**. This is the strongest practical differentiator. - -### 4. Context model: relative, typed -The current model **narrows as you descend** (Go `text/template` `.`/`$`, Mustache family — the -*shape* is not novel), with `::`/`@root` as the escape hatch and a deliberate one‑level -step‑back in value‑extension bodies (so `@if(flag){{ @(Title) }}` still sees the surrounding -model). What *is* uncommon is pairing this relative model with **static typing threaded through -the narrowing** at compile time. - -### 5. Compiled and fast -Member access and embedded C# are checked at compile time, and rendering walks a pre‑compiled -document with no per‑call activation/section/DI overhead. On the repo's component‑heavy -home‑page benchmark it renders **markedly faster than ASP.NET Core Razor (about double the -throughput) with fewer allocations** — see [Architecture → Performance](architecture.md#performance-characteristics) -and [src/Heddle.Performance](../src/Heddle.Performance). The win is structural (no -per‑component partial/view‑component machinery), so it's largest on component‑dense pages and -narrower on text‑heavy ones. +## 4. Weaknesses ranked by strategic severity + +1. **Raw‑by‑default output** — disqualifying for the HTML use case in any security‑conscious + org; also the cheapest reputational loss ("failed our review in five minutes"). Fix in the + 2.0 breaking window via output profiles. +2. **Expression power welded to Roslyn** — the root cause behind four separate symptoms: no + sandbox, no AOT, slow first compile, and `Count > 0` requiring full‑trust C#. One + investment (§6 keystone) resolves all four. +3. **Everyday ergonomics** — no `else`/`elif` (the most‑felt gap in real templates), no named + arguments/props (caps the component story that is supposed to be the differentiator), a + `ForModel`‑shaped counted loop that requires C# to construct. +4. **Tooling** — no LSP/completion/diagnostics‑in‑editor; per‑use‑site type errors *need* + good tooling more than ordinary engines do, because the error's cause and location are in + different files by design. +5. **Ecosystem & bus factor** — single author, .NET‑only, no corpus. Not fixable by fiat; + fixable only by picking a niche small enough to win and documenting relentlessly (already + underway). +6. **Sigil density** — real but overweighted by first impressions; highlighting + an LSP + defuse most of it. Spend here last. --- -## Honest weaknesses - -- **Sigil density / readability.** `@% %@`, `{{ }}`, ``, `-> ()`, `:: Type`, - `::Member`, `@<<{{ }}`, `@{ }@`, `@:`, `@\`, `@@` — and the punctuation is overloaded (`:` is - chain delimiter *and* base separator *and* half of `::`; `{{ }}` is body *and* import‑path - wrapper *and* definition body). Real templates read densely. (Whitespace trimming `@\` is less - of a tax than it first appears — HTML tolerates the whitespace, so it's mostly needed in the - declaration preamble.) -- **Missing ergonomic basics.** No `else`/`elif` (compose `@if`/`@ifnot`, often nested); no - **named extension arguments** (a call takes one model value or a C# expression). *Correction - to an earlier draft:* "no named slots" was wrong — named regions are simply multiple - definitions a layout calls; that's a real strength, not a gap. -- **Security is all‑or‑nothing.** `AllowCSharp` is the whole story: off = safe but you lose - typed expressions; on = arbitrary C# execution. No sandbox like Scriban/Liquid, so it's - unsuitable for **untrusted, user‑supplied** templates. Fine for trusted first‑party templates - (the intended use). -- **Supportability / ecosystem.** Sophisticated implementation (8‑mode ANTLR lexer, Roslyn - codegen, expression‑tree accessors, custom string builders, bespoke Ace editor mode) - for what looks like a single‑author project — real **bus‑factor** and a thin ecosystem. - .NET‑only; no Stack Overflow corpus; docs were near‑absent before this pass (now addressed). The novelty cuts - both ways: elegant but unfamiliar, so the pool who can maintain/extend it is small. -- **Debuggability (inferred).** Embedded C# compiles via Roslyn and member access via - expression trees; a broken expression likely won't map back to a template line as cleanly as - an interpreted engine. Worth verifying before trusting in anger. -- **Extension‑authoring floor.** Registering a Liquid/Scriban filter is a lambda; a Heddle - extension is a class with `InitStart` type negotiation (`ExType`), *both* `ProcessData` and - `RenderData`, and `Scope` transforms. More powerful, but the simple case isn't simple. +## 5. Direction options + +### A. The typed component engine for .NET *(double down on composition)* + +Own the empty quadrant of §3.3: typed, fast, framework‑independent rendering for emails, +documents, SSR fragments, static sites. **Demands:** named/typed props, parameterized slots, +encode‑by‑default HTML profile, `else`, LSP. **Wins:** teams already typing their models who +hate stringly‑typed templates; the benchmark story sells itself. **Risk:** medium — it's an +extension of what exists; the competition (Razor‑out‑of‑band) is genuinely weak. + +### B. Safe embeddable templating *(compete with Scriban/Fluid for user templates)* + +A sandboxed tier: native expressions over whitelisted members, resource limits, no C#. +**Demands:** the native expression language *plus* member whitelisting, quotas, and a security +posture worth auditing. **Wins:** SaaS products embedding user templates. **Risk:** high — +security is a promise that must be kept forever, the incumbents are good and free, and +Heddle's typing advantage matters least exactly here (user templates bind late by nature). + +### C. Build‑time compilation / AOT *(the T4 successor)* + +A source generator (or MSBuild task) that compiles `.heddle` at build time into C# — real +stack traces, zero startup cost, Native‑AOT‑ and trimming‑clean, debuggable by stepping into +generated code. **Demands:** a second compiler backend (emit C# instead of building the +runtime document) — significant but well‑bounded work; the parse/walk front end is shared. +**Wins:** AOT services, codegen users, everyone who resents T4. **Risk:** medium — a second +backend is a maintenance commitment for a one‑maintainer project; mitigated by sharing the +front end and treating the runtime path as the reference semantics. + +### D. Conservative polish *(ergonomics only, no strategic bet)* + +`else`, loop sugar, docs, warnings; no new tiers or backends. **Wins:** current users. +**Risk:** lowest effort, highest opportunity cost — none of §4's top two debts move, and the +language stays confined to its current audience. + +These are not mutually exclusive — they share a dependency structure, which is the basis of +the recommendation. --- -## Versus competitors - -| | **Heddle** | Razor | Liquid / Scriban | Handlebars / Mustache | Go `text/template` | -| --- | --- | --- | --- | --- | --- | -| Execution | Compiled to an exec‑ready document | Compiled | Interpreted | Interpreted | Interpreted | -| Typing | **Static, per use site** | Static | Dynamic | Dynamic | Dynamic | -| Logic in templates | Full C# (opt‑in) | Full C# | Sandboxed filters | Logic‑less + helpers | Limited + funcs | -| Control flow | **Extensions (a library)** | Keywords | Tags | Block helpers | Keywords | -| Context model | Relative: current / `::`root | Absolute (`Model.X`) | Mostly global | Relative stack | Relative (`.` / `$`) | -| Composition | Definition inheritance + override | Layouts / sections / partials | Partials / includes | Partials | Heddle / blocks | -| Untrusted templates | No (or no‑C# mode) | No | **Yes (sandboxed)** | **Yes (logic‑less)** | Partial | -| Reach / ecosystem | .NET only, small | .NET, excellent | Large | Large, polyglot | Large | - -Notes: -- **vs Razor** — comparable expressive power (both compiled, full C#), but Heddle's composition is - decoupled where Razor's sections are not, and Heddle is faster on component‑heavy pages. Razor - wins decisively on tooling, debugging, and ecosystem. -- **vs Liquid/Scriban** — those are sandboxed and safe for untrusted templates; Heddle is not. Heddle - is compiled/typed where they are dynamic/interpreted. -- **vs Handlebars/Mustache** — Heddle's extensions ≈ helpers, but typed and compiled; those are - dynamic, logic‑less, and polyglot. -- **vs Go templates** — closest on the context model (`.`/`$` ≈ current/`::`), but Go is dynamic - and interpreted; Heddle adds static typing and an inheritance/late‑binding composition model. -- **vs T4** — same "compiled + full C#" idea, far cleaner composition. +## 6. Recommendation + +**Primary direction: A — the typed component engine for .NET — with C as its second phase and +B kept open but not promised.** A is where Heddle's genuine differentiators (per‑site typing, +free composition, render speed) buy the most, against the weakest competition. C compounds A +(AOT + debuggability + startup are all adoption objections A will hit). B is only credible +after the keystone exists, and should be re‑evaluated then rather than committed to now. + +### The keystone: a native expression tier + +One design investment unlocks every direction: a **small, statically typed expression +language** evaluated without Roslyn — comparisons, boolean logic, arithmetic, string +concatenation/interpolation, indexing, null‑coalescing, and calls to registered functions, +compiled to the same expression‑tree delegates member paths already use (and, in the §5C +backend, to plain C#). Consequences: + +- `@if(@Comments.Count > 0)` works with `AllowCSharp = false` → the security model gains a + real middle tier (path/native/full‑C#), making a future sandbox (B) *possible*; +- Roslyn leaves the common path → first‑compile cost drops, and the AOT story (C) stops being + blocked by design; +- `AllowCSharp = true` remains the unchanged full‑power escape hatch — the ceiling is not + lowered, the floor is raised. + +The grammar is hand‑rolled and already lexes C# expressions; a native tier is a *subset* +carve‑out, not new lexer territory. + +### Sequenced roadmap + +**Phase 0 — the 2.0 breaking window (safety + ergonomics).** Small, high‑visibility, mostly +independent items; ship together because two are breaking: + +1. **Output profiles**: `html` profile encodes `@(...)` by default with `@raw(...)` opt‑out; + `text` profile preserves today's semantics. *(Breaking; the reason 2.0 exists.)* +2. **`else` / `elif`** — recognized after an `@if` body (the `CALL_RETURNED` lexer mode + already dispatches on what follows a call; this is the natural place to hang it). +3. **Counted‑loop sugar** — `@for(@5)` / range forms so `ForModel` construction stops + requiring C#. +4. Compile **warning** for calling a `-> ` definition directly (double render). +5. Optional line‑trim for whole‑line directives. + +**Phase 1 — the keystone.** The native expression tier, plus **named arguments/props** for +definitions (`@card(Article, style: "wide")` with props declared in the definition header) and +**parameterized slots**. This is the release that makes "component engine" true, and it is +where grammar design time should be spent most carefully. + +**Phase 2 — the build‑time backend (§5C).** Source generator emitting C#; Native AOT and +trimming supported; template debugging via generated code. Runtime path remains for dynamic +scenarios and stays the semantic reference. + +**Phase 3 — tooling.** LSP built on the existing `ProvideLanguageFeatures` token stream: +completion for members (the type information already exists at compile time — few template +languages can offer *correct* completion; Heddle can), hover types, go‑to‑definition across +imports, inline per‑use‑site error mapping. Plus the `SimpleExtension`/lambda registration +floor for extension authors. + +Each phase is independently shippable and independently valuable — important for a +one‑maintainer project: stopping after any phase leaves the language strictly better. + +### Non‑goals (explicit) + +- **Replacing Razor inside ASP.NET MVC** — unwinnable, unnecessary. +- **Polyglot ports** of the engine — the JS parser stays a *highlighting* artifact only. +- **A logic‑less mode** — contradicts the thesis; Mustache exists. +- **Widening the sigil redesign** — aliases and tooling, not a new surface; the installed + base and docs are worth more than a prettier `@<<`. --- -## Verdict +## 7. Open design questions -- **Best fit:** performance‑sensitive, first‑party .NET HTML/text generation by a team that - values typed templates and component‑style composition and will invest in the bespoke - toolchain. -- **Poor fit:** untrusted user‑supplied templates, polyglot stacks, or teams wanting - batteries‑included tooling and a large hiring pool. +1. **`else` surface**: block‑suffix (`@if(x){{ … }} @else {{ … }}`) vs chain‑integrated — + must compose with `@ifnot` and chains without ambiguity in `CALL_RETURNED`. +2. **Props declaration syntax**: in the definition header (``) + vs annotation‑style; how props interact with the single model parameter and abstract typing. +3. **Native expression tier boundary**: does it allow *any* method calls (e.g. `.Count()`, + `.ToUpper()`) via a registered‑function whitelist, or properties/indexers only? The answer + determines whether B (sandbox) is ever credible — resolve before Phase 1 freezes. +4. **Encoding profile granularity**: per template, per compile options, or per file extension + (`.heddle.html` vs `.heddle.txt`)? +5. **Async/streaming API**: `IBufferWriter`/UTF‑8 sink alongside `string Generate` — + Phase 2 or opportunistic earlier? -The ceiling is high and now measurable; what limits adoption is the **floor** (ergonomics) and -the **ecosystem**, not the engine. +--- -## If I were evolving it (highest‑leverage, ergonomics over features) +## Verdict -1. `if/else`/`elif` (a syntax or extension) — the most felt gap. -2. Named extension arguments. -3. Reduce sigil overload / a friendlier surface. -4. A real "first 10 minutes" doc + an LSP/editor story. -5. Decide and document the security posture (keep all‑or‑nothing, or add a restricted mode). +The 1.0 assessment stands: high ceiling, limiting floor. What this re‑assessment adds is that +the ceiling is *strategically placed* — Heddle is already in the quadrant the industry is +moving toward, in an ecosystem where that quadrant is otherwise empty. The core language needs +almost no revision (§2 verdicts are overwhelmingly Keep/Polish); the work is to fix the unsafe +default, raise the expression floor, and let tooling dissolve the surface tax. Direction A, +keystone first. diff --git a/docs/building.md b/docs/building.md index e21a5b00..80a13aad 100644 --- a/docs/building.md +++ b/docs/building.md @@ -128,8 +128,31 @@ tokens) and is skipped on fork pull requests. - **Production releases are tag-driven.** Pushing a `vX.Y.Z` tag publishes that exact version to nuget.org and npmjs.org (as `latest`, with npm provenance) and creates a matching GitHub Release. Merging to `main` only builds and tests — it does not publish. -- **[Documentation](../.github/workflows/docs.yml)** — builds this site and deploys it to - GitHub Pages. +- **[Documentation](../.github/workflows/docs.yml)** — builds this site (including the WebAssembly + demo bundle) and deploys it to GitHub Pages. Pull requests build and run the demo smoke suite but + do not deploy. +- **[Integration samples](../.github/workflows/samples.yml)** — a `fail-fast: false` matrix, one job + per `samples/` project, running each in capture mode and comparing against its golden. + +## The integration sample gallery + +The repo-root [`samples/`](../samples/README.md) folder holds ten small, complete, runnable projects — +one per supported way to integrate Heddle (SSR, definition libraries, dynamic models, sandboxed user +templates, safe output, custom extensions, component libraries, build-time codegen, precompilation, and +streaming). They double as the engine's end-to-end test suite: each captures deterministic output that CI +compares against a committed golden, so a broken sample *is* a failed integration test. + +Run one interactively, or in the CI capture mode: + +```bash +dotnet run --project samples/dynamic-models # human mode +dotnet run --project samples/dynamic-models -c Release -- --capture out +bash samples/tools/compare-golden.sh samples/dynamic-models # byte-compare vs golden/ +``` + +`UPDATE_GOLDEN=1 bash samples/tools/compare-golden.sh samples/` regenerates a golden (a review event, +never a silent fix). Adding a sample is one folder + one `samples.yml` matrix entry + one index row — see the +[gallery README](../samples/README.md#adding-a-sample). ## Regenerating the parser diff --git a/docs/built-in-extensions.md b/docs/built-in-extensions.md index abaf05b4..6c681fff 100644 --- a/docs/built-in-extensions.md +++ b/docs/built-in-extensions.md @@ -37,9 +37,12 @@ present (renders the body). ```heddle @if(IsFeatured){{ Featured }} -@if(@model.Comments.Count > 0){{

Comments

}} +@if(Comments.Count > 0){{

Comments

}} ``` +> The parameter of `@if`/`@for` is any [native expression](native-expressions.md), so +> `@if(Count > 0)` and `@if(Price * Quantity > 100)` work without the `@` C# tier. + Because a non‑null, non‑bool value counts as present, **`@if(member)` is the idiomatic "if present" check** for a nullable reference: `@if(Summary){{

@(Summary)

}}` renders only when `Summary` is set. The body runs in the caller's context (see @@ -57,8 +60,61 @@ non‑null model renders nothing — so `@ifnot(member)` is the idiomatic "if ab @ifnot(Summary){{

No summary yet.

}} ``` -> There is no `else`/`elif`. Combine `@if`/`@ifnot` (often nested) for the branches you need — -> see [Patterns → conditionals](patterns.md#presence-checks-and-and-conditions). +### `elif` (alias `elseif`) +[ElifExtension.cs](../src/Heddle/Extensions/ElifExtension.cs) · input: `bool` (or any object) + +Continues a branch set opened by a preceding `@if`/`@ifnot`. It renders its body only when no +earlier branch of the set fired **and** its own condition is truthy. Once a branch has fired, +every later `@elif`/`@else` in the set renders nothing (its condition is still evaluated, only +its body is skipped). `elseif` is an exact alias. + +```heddle +@if(IsFeatured){{ Featured }} +@elif(IsArchived){{ Archived }} +``` + +With no preceding `@if`/`@ifnot` at the same level, `@elif` behaves exactly like `@if` (it +starts a new set) and the compiler emits a `HED3002` warning. + +### `else` +[ElseExtension.cs](../src/Heddle/Extensions/ElseExtension.cs) · no condition + +The terminal branch of a set: it renders its body when no earlier branch fired, then closes the +set. `@else` takes no parameter — a supplied one is evaluated and ignored (`HED3004`). An +`@else` with no open set at the same level (an orphan, or a second `@else` after the set was +already closed) is a **compile error** `HED3003` ("`@else` has no matching `@if`"). + +```heddle +@if(IsFeatured){{ Featured }} +@elif(IsArchived){{ Archived }} +@else(){{ Regular }} +``` + +#### Branch sets + +`@if`/`@ifnot`, `@elif`/`@elseif` and `@else` are ordinary standalone extensions; there is no +new syntax and no `{{endif}}`. A **branch set** is recognized at compile time by document order +at one body level: an opener (`@if`/`@ifnot`) followed by any run of `@elif` blocks and at most +one `@else`. Only the winning branch's body renders. + +- **`@else` binds to the set state, not to a specific `@if`.** A set is "satisfied" once any + branch's condition was true (an `@if` whose body renders empty still satisfies it). `@else` + fires only when the set is unsatisfied. +- **Text between the blocks of one set is never rendered.** It is stripped at compile time; + a stripped gap that holds anything beyond whitespace draws a `HED3001` warning. Text *before* + the opener and *after* the last branch renders normally. Comments (`@* … *@`) between blocks + live on the hidden channel and never affect the set. +- **A new `@if` starts a new set**, and a **non‑branch block between branches ends the set for + the purposes of text stripping** but leaves the set state intact — so `@if(A){{…}} @date(D){{…}} @else(){{…}}` + keeps `@else` bound to the open set while the text around `@date` still renders. +- **Imports.** Blocks spliced in by `@<<{{ … }}` are siblings of the importing body: they join + its branch sets and its compile‑time scan. Blocks parsed in by the `@import()` extension arrive + after that scan, so they coordinate at runtime only. +- **Isolation.** Each `@list`/`@for` iteration, each nested body, and each `@partial` gets its + own set state — an inner set can never satisfy or clear an outer one. + +Custom extensions can read or drive a set's state through the public `BranchState` channel — see +[Writing Custom Extensions → the local context channel](custom-extensions.md#the-local-context-channel). --- @@ -81,12 +137,36 @@ the source implements `ICollection`, the count is used to pre‑size the outp ``` ### `for` -[ForIndexExtension.cs](../src/Heddle/Extensions/ForIndexExtension.cs) · input: `ForModel` +[ForIndexExtension.cs](../src/Heddle/Extensions/ForIndexExtension.cs) · input: `ForModel` or `int` A counted loop. The model is a `ForModel { Start?, Last, Step? }` ([Heddle.Models](../src/Heddle/Models)); it iterates `i = Start (default 0)` while `i < Last`, incrementing by `Step` (default 1). The loop index is exposed to the body as the -chained value (referenceable as `chained` in embedded C#). +chained value (referenceable as `chained` in embedded C#, or output directly with `@out()`). + +**Counted loops without C#.** `@for` also accepts a plain `int`, iterating `0…n−1`, so the +common cases need no embedded C# at all: + +```heddle +@for(3){{
  • @out()
  • }} @* renders
  • 0
  • 1
  • 2
  • *@ +@for(Count){{ ... }} @* Count is an int member of the model *@ +``` + +A negative or zero count renders empty (the loop condition is false immediately), matching a +data‑driven `Count = 0`. + +**`range(start, last[, step])`.** The default [function registry](native-expressions.md) +includes `range`, which builds a `ForModel` — so `@for(range(...))` gives start/step control +with no new syntax: + +```heddle +@for(range(2, 8, 3)){{ [@out()] }} @* [2][5] — start 2, step 3, last-exclusive *@ +``` + +`step` must be positive: a zero or negative literal step is a compile error (**HED4001**), and a +non‑positive step known only at render throws. See [native expressions](native-expressions.md#range). + +The C# tier still works for computed models: ```heddle @for(@new ForModel() { Last = model.Articles.Count(), Step = 3 }) @@ -176,25 +256,49 @@ culture), falling back to `ToString()`. ## Output and context ### Empty / unnamed -[EmptyExtension.cs](../src/Heddle/Extensions/EmptyExtension.cs) · name: `""` +[EmptyExtension.cs](../src/Heddle/Extensions/EmptyExtension.cs) · names: `""`, `raw` The extension with the empty name backs the unnamed call form `@(...)`. If it has a body it renders the body; otherwise it stringifies the current model (or empty when `null`). This is the workhorse behind `@(Title)`, `@()`, etc. -> The unnamed `@(...)` form is **not** HTML‑encoded. For encoded text output use -> [`string`](#string) (or `html` below). +Its output depends on the effective [output profile](#output-profiles): + +- Under **`OutputProfile.Text`** (the 1.x default) the unnamed `@(...)` form is **not** + HTML‑encoded — raw text/JSON/code output, exactly as before. +- Under **`OutputProfile.Html`** a *bodiless* unnamed `@(value)` is HTML‑encoded by default + (the compiler resolves it to `html` below). A *bodied* `@(value){{…}}` is unchanged — it is a + raw rescoping container, so its literal body markup is never encoded; only value leaves inside + it encode. Use [`raw`](#raw) to opt a trusted value out. + +### `raw` +[EmptyExtension.cs](../src/Heddle/Extensions/EmptyExtension.cs) · name: `raw` + +A second name on the empty extension: `@raw(value)` always emits the value verbatim, under +**both** profiles. Under `Html` it is the trusted‑value opt‑out (the analogue of Razor's +`Html.Raw`, Handlebars' `{{{ }}}`, Jinja's `|safe`); under `Text` it is a harmless alias. + +```heddle +@raw(TrustedHtmlFragment) +``` + +> Never pass untrusted input through `@raw(...)` under `Html` — that reopens the XSS hole the +> profile closes. ### `html` [EmptyHtmlExtension.cs](../src/Heddle/Extensions/EmptyHtmlExtension.cs) · name: `html` · HTML‑encoded -Same behavior as the empty extension, but HTML‑encodes its output (`[EncodeOutput]`). Use it -when you want the "just stringify the value" behavior *with* encoding. +Same behavior as the empty extension, but always HTML‑encodes its output (`[EncodeOutput]`), +under both profiles. Use it when you want the "just stringify the value" behavior *with* +encoding, independent of the profile. ```heddle @html(UserSuppliedText) ``` +> Under `Html`, wrapping a value in `@html(...)` inside the unnamed sink (`@(html(X))`) encodes +> twice and raises the [`HED2003`](#html-encoding) warning — drop the `html()` or use `@raw`. + ### `out` [OutExtension.cs](../src/Heddle/Extensions/OutExtension.cs) · name: `out` @@ -211,6 +315,16 @@ chained data. This is how a definition surfaces the caller's inline content. central `@out()`, and each page supplies the content. See [Language Reference → composition](language-reference.md#inheritance-and-override-childbase). +**Two modes.** `@out` has a second, *slot* mode. When the enclosing definition declares a +[slot parameter](language-reference.md#parameterized-slots-out-type) (`out:: Type`), `@out(expr)` +renders the caller's body **once per execution** with `expr` (typed as the slot type) as its model +— the picker pattern, `@list(Options){{ @out(this) }}`. In slot mode every `@out` must pass a value +(a bare `@out()` is **HED5013**) and is bodiless (**HED5018**). + +**`@out` with a value needs a slot.** Outside a slot‑declaring body, `@out` takes **no** argument — +`@out(X)` (formerly accepted and silently ignored) is now the compile error **HED5012**. The fix is +to drop the argument (`@out()`) or to declare `out:: Type` and mean it. + ### `swap` [SwapExtension.cs](../src/Heddle/Extensions/SwapExtension.cs) · name: `swap` @@ -254,9 +368,29 @@ resolve unqualified identifiers. ### `import` [ImportExtension.cs](../src/Heddle/Extensions/ImportExtension.cs) · name: `import` -Compile‑time include: reads another template file (relative to `RootPath`) and parses its -**definitions** into the current parse context. This is the extension behind the -[`@<<{{ path }}`](language-reference.md#imports--) sugar. Emits no output of its own. +Compile‑time include that re‑parses another template file (relative to `RootPath`) into the +extension body's own isolated context. It is **not** the machinery behind `@<<` — the two are +[independent import implementations](language-reference.md#imports--) with different +behavior. `@import()` merges nothing into the importing document (its parsed definitions are not +callable afterwards); its practical effect is to validate the target file (its errors surface). +Emits no output of its own. **Prefer [`@<<{{ path }}`](language-reference.md#imports--) +for sharing definitions** — it merges definitions, re‑bases the imported chains, and carries +default outputs. + +### `profile` +[ProfileExtension.cs](../src/Heddle/Extensions/ProfileExtension.cs) · name: `profile` + +Compile‑time directive that sets the effective [output profile](#output-profiles) for output +compiled **after** it in document order (bodies, partials, and imports created afterwards +inherit it). The value is read from the body and must be `text` or `html` +(case‑insensitive); any other value is the [`HED2001`](#html-encoding) error. Emits no output. + +```heddle +@profile(){{ html }} @* everything below now HTML‑encodes the unnamed @(...) form *@ +``` + +Placing `@profile()` after some unnamed output has already been compiled still applies the +flip but raises the [`HED2002`](#html-encoding) warning — keep it at the top of the template. --- @@ -284,15 +418,18 @@ passing the current model. | --- | --- | --- | --- | --- | | `if` | IfExtension | `bool`/any | – | content shown when true | | `ifnot` | IfNotExtension | `bool`/`null` | – | content shown when false/null | +| `elif` / `elseif` | ElifExtension | `bool`/any | – | next branch of a set (else‑if) | +| `else` | ElseExtension | – | – | terminal branch of a set | | `list` | ListExtension | `IEnumerable` | – | per‑element template | -| `for` | ForIndexExtension | `ForModel` | – | per‑iteration template | +| `for` | ForIndexExtension | `ForModel` / `int` | – | per‑iteration template | | `date` | DateExtension | `DateTime` | ✔ | date format string (def. `d`) | | `time` | TimeExtension | `DateTime` | ✔ | time format string (def. `t`) | | `int` | IntegerExtension | `int`/`long` | ✔ | numeric format string | | `money` | MoneyExtension | `decimal` | ✔ | culture name | | `guid` | GuidExtension | `Guid` | – | GUID format specifier | | `string` | StringExtension | `string`/any | ✔ | fallback when null | -| *(empty)* | EmptyExtension | any | – | optional body, else stringify model | +| *(empty)* | EmptyExtension | any | profile¹ | optional body, else stringify model | +| `raw` | EmptyExtension | any | – | trusted‑value opt‑out (never encodes) | | `html` | EmptyHtmlExtension | any | ✔ | optional body, else stringify model | | `out` | OutExtension | chained | – | template over chained data | | `swap` | SwapExtension | – | – | body with model/chained swapped | @@ -300,22 +437,59 @@ passing the current model. | `model` | ModelExtension | – | – | model type name | | `using` | UsingExtension | – | – | namespace to import | | `import` | ImportExtension | – | – | file to include (definitions) | +| `profile` | ProfileExtension | – | – | output profile (`text`/`html`) | | `partial` | PartialExtension | model | – | template name to render | +¹ The bodiless unnamed `@(...)` form encodes under `OutputProfile.Html` and is raw under +`OutputProfile.Text` — see [Output profiles](#output-profiles). + --- +## Output profiles + +The **output profile** decides whether the bodiless unnamed `@(...)` form HTML‑encodes by +default. It is selected programmatically — there is no file‑extension inference: + +- **`OutputProfile.Text`** (the 1.x default) — `@(value)` emits raw text. This is the profile + for text, JSON, and code generation and keeps every existing template byte‑identical. +- **`OutputProfile.Html`** — a bodiless `@(value)` HTML‑encodes by default (XSS‑safe output); + `@raw(value)` opts a trusted value out; a bodied `@(value){{…}}` stays a raw rescoping + container (only value leaves inside it encode). + +Set it two ways (both feed the same effective profile; the directive wins for output after it): + +- Host: [`TemplateOptions.OutputProfile`](csharp-api.md#templateoptions) (and the + `TemplateResolver` default‑profile constructor). +- Template: the [`@profile()`](#profile) directive. + +Encoding always happens at the **emitting leaf** and exactly once: a value flowing through a +container (`list`, `if`, `out`, …) or a nested producer (`@(upper(X))`) is encoded a single +time by the leaf that writes it. Wrapping a value in an already‑encoding extension under the +unnamed sink (`@(html(X))`) encodes twice and raises the `HED2003` warning. + +> **1.x → 2.0:** the default profile stays `Text` in 1.x. In the 2.0 window it flips to `Html` +> and the encoder moves from `WebUtility.HtmlEncode` to a pluggable +> `HtmlEncoder.Create(UnicodeRanges.All)`. Hosts that need today's behavior set +> `OutputProfile.Text` explicitly (or mark trusted spots with `@raw`). + ## HTML encoding -Encoding is decided **per extension**, not globally: +Encoding is decided **per extension** plus the profile: - Extensions marked **`[EncodeOutput]`** (and deriving from `AbstractHtmlExtension`) HTML‑encode their output with `WebUtility.HtmlEncode`. These are `date`, `time`, `int`, - `money`, `string`, and `html`. -- The unnamed `@(...)` output and non‑HTML extensions (`guid`, `out`, `list`, `if`, …) emit - their text **as‑is**. -- The **`[NotEncode]`** attribute, applied to a *model property*, marks that property's value - as pre‑trusted so it is not encoded even when flowing through an encoding extension. See - [src/Heddle/Attributes/NotEncodeAttribute.cs](../src/Heddle/Attributes/NotEncodeAttribute.cs). - -To emit user‑controlled text safely, prefer `@string(...)` or `@html(...)` over the bare -`@(...)` form. + `money`, `string`, and `html` — under **both** profiles. +- The unnamed `@(...)` output encodes only under `OutputProfile.Html` (see + [Output profiles](#output-profiles)); `@raw(...)` and non‑HTML extensions (`guid`, `out`, + `list`, `if`, …) emit their text **as‑is** under both profiles. + +Profile diagnostics: + +- **`HED2001`** (error) — `@profile()` names a value other than `text`/`html` (or is empty). +- **`HED2002`** (warning) — `@profile()` appears after output has already been compiled in the + same scope; the flip still applies, but earlier output kept the previous profile. +- **`HED2003`** (warning) — an `[EncodeOutput]` producer feeds the auto‑encoding unnamed sink + under `Html` (`@(html(X))`), encoding twice. Remove the inner call or use `@raw(...)`. + +To emit user‑controlled text safely, run under `OutputProfile.Html` (or prefer `@string(...)` +/ `@html(...)`) rather than the bare `@(...)` form under `Text`. diff --git a/docs/csharp-api.md b/docs/csharp-api.md index ff7cecbc..e747e5ee 100644 --- a/docs/csharp-api.md +++ b/docs/csharp-api.md @@ -72,6 +72,53 @@ Behavior notes: string html = template.Generate(model); ``` +### Streaming: `Generate` into a sink + +Alongside the string overload, `HeddleTemplate` renders into a sink with **no full‑output +string materialization** — the structural allocation high‑throughput server‑side rendering +wants to remove: + +```csharp +public void Generate(object data, TextWriter writer, object chained = null, object callerData = null); +public void Generate(object data, IBufferWriter writer, object chained = null, object callerData = null); +``` + +- The `TextWriter` overload writes characters through to whatever buffering the writer already + has (a `StreamWriter`, ASP.NET Core's `HttpResponseStreamWriter`, a `StringWriter`). +- The `IBufferWriter` overload writes **UTF‑8** directly into the writer's own spans — a + `PipeWriter` qualifies (`PipeWriter : IBufferWriter`), which is the ASP.NET Core + `Response.BodyWriter` shape. + +Both overloads are **write‑through**: the engine adds no buffering of its own, and it never +flushes, completes, or disposes the sink — **the caller owns the sink's lifecycle.** They share +every compile‑guard exception with the string path, treat a null model the same way, and throw +`ArgumentNullException` when `writer` is null. The string path is unchanged and byte‑identical. + +There are **no async render methods** by design. The sanctioned backpressure pattern is the +ecosystem's own — render synchronously, then let the *host* flush asynchronously: + +```csharp +// ASP.NET Core minimal API — render straight into the response, host flushes. +app.MapGet("/page", async (HttpContext ctx) => +{ + ctx.Response.ContentType = "text/html; charset=utf-8"; + template.Generate(model, ctx.Response.BodyWriter); // synchronous, no intermediate string + await ctx.Response.BodyWriter.FlushAsync(); // the host owns the flush +}); +``` + +For very large pages, flush periodically using `PipeWriter.UnflushedBytes` to bound the buffer +window. The sink is **single‑owner for the duration of the call**: don't touch it from another +thread until `Generate` returns, and never pass one sink to two simultaneous renders. Different +sinks on different threads against the same template are fully supported. + +> **Source‑compatibility note.** A call written as the positional literal +> `template.Generate(data, null)` is now **CS0121** (ambiguous between the `TextWriter` and +> `IBufferWriter` overloads — `null` is equally convertible to both). This is a +> compile‑time‑only, self‑announcing change; fix it with `Generate(data)` or +> `Generate(data, chained: null)`. Binary compatibility is unaffected — existing assemblies keep +> binding the string overload — and any argument statically typed `object` still exact‑matches it. + ### Compilation methods | Method | Description | @@ -125,7 +172,11 @@ Controls where templates are read from and which features are enabled | `RootPath` | `AppContext.BaseDirectory` | Base directory for template files and imports/partials. | | `TemplateName` | `""` (ctor arg) | The template's name (file stem). Set via `new TemplateOptions("name")`. | | `FileNamePostfix` | `""` | Suffix appended to the name, e.g. `".heddle"`. | -| `AllowCSharp` | `false` | Enables embedded C# (`@(...)` expressions, `@new`, LINQ, typed `@model()`). Required for most non‑trivial templates. | +| `ExpressionMode` | `Native` | Selects the expression tier: `MemberPathsOnly`, `Native` (sandbox‑safe operators/functions — see [Native Expressions](native-expressions.md)), or `FullCSharp` (adds the inner‑`@` Roslyn tier). | +| `Functions` | `null` (= `FunctionRegistry.Default`) | Functions callable from native expressions; see [Native Expressions](native-expressions.md#registered-functions). | +| `OutputProfile` | `Text` | Selects whether the unnamed `@(...)` output HTML‑encodes by default: `Text` (raw, the 1.x default) or `Html` (bodiless `@(value)` encodes; `@raw` opts out). Inherited by bodies/partials/imports; also settable per template with [`@profile()`](built-in-extensions.md#profile). See [Output profiles](language-reference.md#output-profiles). Participates in `Equals`/`GetHashCode`. | +| `TrimDirectiveLines` | `false` | When `true`, a whole‑line directive that produces no output (`@using`, `@model`, `@profile(){{…}}`, `@import`, `@% … %@` definitions, `@<<` imports, whole‑line comments, and any extension whose `InitStart` returns `null`) swallows its line — leading indentation, trailing spaces, and one line terminator. Inherited by child compiles. `false` keeps 1.x whitespace byte‑exact; flips to `true` in the 2.0 window. Participates in `Equals`/`GetHashCode` (it changes rendered bytes, so it keys template caches). Compile‑time only. See [Whitespace trimming](language-reference.md#whitespace-trimming-). | +| `AllowCSharp` | `false` | Bridge over `ExpressionMode`: `true` == `FullCSharp`. Enables embedded C# (`@( @expr )`, `@new`, LINQ, typed `@model()`). Setting `false` leaves `MemberPathsOnly` untouched, otherwise selects `Native`. | | `MaxRecursionCount` | `100` | Upper bound on definition recursion depth. | | `EnableFileChangeCheck` | `false` | Install a `FileSystemWatcher` and recompile on file change. | | `ProvideLanguageFeatures` | `false` | Parse in a tooling mode that emits a token list for editors/highlighters (used by the IDE integrations). | @@ -143,7 +194,36 @@ var options = new TemplateOptions("home") ``` `TemplateOptions` implements value equality over (`TemplateName`, `RootPath`, -`FileNamePostfix`), so it can be used as a cache key. +`FileNamePostfix`, `OutputProfile`, `TrimDirectiveLines`), so it can be used as a cache key — +two option sets that differ only by profile *or* by trimming are distinct keys (they render +different bytes). + +### Choosing the output profile per template + +The profile is selected programmatically — there is **no** file‑extension inference. A host +that wants extension‑driven profiles maps extensions to options in its own resolver wiring: + +```csharp +OutputProfile ProfileFor(string path) => + Path.GetExtension(path) is ".html" or ".htm" or ".xml" + ? OutputProfile.Html + : OutputProfile.Text; + +var options = new TemplateOptions(Path.GetFileNameWithoutExtension(path)) +{ + RootPath = root, + FileNamePostfix = Path.GetExtension(path), + OutputProfile = ProfileFor(path) +}; +``` + +`TemplateResolver` also accepts a default profile and trimming setting: +`new TemplateResolver(rootPath, checkFileChange, OutputProfile.Html)` and the four‑parameter +`new TemplateResolver(rootPath, checkFileChange, OutputProfile.Html, trimDirectiveLines: true)`. +The existing two‑parameter constructor is unchanged and keeps `(Text, false)`; a per‑call +`CompileContext`'s options override both resolver defaults, and the resolver caches each +template under a key suffixed by profile **and** trimming so one resolver can serve every +combination without a collision. --- @@ -163,6 +243,7 @@ Key members: | Member | Meaning | | --- | --- | | `TemplateOptions Options` | The options this context compiles with. | +| `OutputProfile OutputProfile` | The effective output profile for items compiled from here on. Initialized from `Options.OutputProfile`; flipped by `@profile()`; snapshotted by child contexts. Compile‑time only. | | `ExType ScopeType` | The current model type. May be changed by the `@model()` extension during compile. | | `ExType RootScopeType` | The root (level‑0) model type. | | `bool Compiled` | Whether the embedded‑C# (Roslyn) compilation step has run. | @@ -258,3 +339,42 @@ if (!template.CompileResult.Success) string html = template.Generate(myBlog); ``` + +--- + +## Build‑time pre‑compilation + +Templates can be pre‑compiled into your assembly at build time so they are **looked up, not +parsed and compiled** at run time — the Razor compiled‑views shape. The runtime types above are +unchanged and remain the semantic reference; the precompiled backend produces byte‑identical +output. The additions live in the `Heddle.Precompiled` namespace: + +- `PrecompiledTemplates` — the process‑wide registry: `Register(assembly)` (repeatable, + idempotent), `Entries` (host‑visible discovery), `TryGet(key, out …)`, the `OnFallback` + diagnostic callback, and the `BindingResolver` hook. +- `PrecompiledMismatchPolicy` on `TemplateOptions` — `Fallback` (default: recompile + warn) or + `Strict` (throw when a precompiled entry exists but cannot be plugged). +- Typed entry points — `{RootNamespace}.HeddleTemplates.{Name}.Generate(model)` — the + recommended host API for templates known at compile time (compile‑checked model, no lookup). + Each typed entry also gains the two **sink overloads** — `Generate(model, TextWriter)` and + `Generate(model, IBufferWriter)` — mirroring `HeddleTemplate` (same no‑materialization + contract, same `Generate(model, null)` CS0121 corner). Building with `HeddleEmitUtf8Pieces=true` + emits pre‑encoded `"…"u8` static pieces that flow to the byte sink with zero transcoding. + +See [Build‑Time Pre‑compilation](precompilation.md) for the generator package, the MSBuild +options, the validation gauntlet, `[ExportFunctions]` binding, and the `heddle` codegen CLI. + +## Advanced: disabling the C# tier for trimmed hosts + +Hosts that never use the Roslyn C# expression tier (`ExpressionMode.FullCSharp` / `AllowCSharp`) — for +example a trimmed WebAssembly bundle — can drop the entire `Microsoft.CodeAnalysis` dependency graph with the +`Heddle.CSharpTierEnabled` **feature switch**: + +- **Runtime:** `AppContext.SetSwitch("Heddle.CSharpTierEnabled", false)` at startup. Templates that require the + C# tier then fail compilation with **HED9001** (collected on the compile result, never thrown); native + expressions, member paths, functions, and every other feature are unaffected. Unset (the default) reads as + `true`, so ordinary hosts see identical behavior. +- **Trimmed publish:** add + ``. The + linker then substitutes the switch to a constant, drops the guarded Roslyn branches, and removes + `Microsoft.CodeAnalysis.*` from the output entirely. This is exactly how the in-browser demo ships Roslyn-free. diff --git a/docs/custom-extensions.md b/docs/custom-extensions.md index 7d42a977..7edfd100 100644 --- a/docs/custom-extensions.md +++ b/docs/custom-extensions.md @@ -51,6 +51,14 @@ public interface IExtension : IDisposable > Implement both `ProcessData` and `RenderData` with equivalent behavior. The engine chooses > between them depending on context (direct rendering vs. value composition). +> **Directive‑style extensions and `TrimDirectiveLines`.** An extension whose `InitStart` +> returns `null` produces no output element and the compiler removes its block from the +> document (the `@using`/`@model`/`@profile` pattern). Such blocks automatically participate in +> [directive‑line trimming](language-reference.md#whitespace-trimming-): when +> `TemplateOptions.TrimDirectiveLines` is on and the block occupies its line by itself, the +> whole line is swallowed. You get this for free — trimming keys on the removal mechanism, not +> on a name list, so there is nothing extra to implement. + ### Helpers from the base class `AbstractExtension` gives you: @@ -73,7 +81,7 @@ At render time you read data from [`Scope`](../src/Heddle/Data/Scope.cs) and wri | `ParentModelData` | The enclosing scope's model. | | `RootData` | The root model (what `::` / `@root` resolve to). | | `CallerData` | Caller context. | -| `Renderer` | The output sink (`Renderer.Render(string)`). | +| `Renderer` | The output sink (`Renderer.Render(string)`). Under the string, `TextWriter`, and UTF‑8 sinks alike — write through `Render(string)` and it just works. | `Scope` is a readonly struct with pure transforms used to build the scope for your subtemplate: @@ -98,6 +106,133 @@ foreach (var item in (IEnumerable)scope.ModelData) } ``` +### Writing to the sink: spans and values + +`scope.Renderer.Render(string)` is all most extensions ever need, and it works unchanged against +every sink (string, `TextWriter`, and the UTF‑8 `IBufferWriter`). When you already have a +`ReadOnlySpan` or a formattable value, opt into the allocation‑free helpers in +[`ScopeRendererExtensions`](../src/Heddle/Data/ScopeRendererExtensions.cs): + +```csharp +using Heddle.Data; + +// Span write — dispatches to the sink's native span path when available, else materializes a string. +scope.Renderer.Render(mySpan); + +// Value format — no intermediate string on the span/UTF-8 tiers (net6+): +// IUtf8SpanFormattable straight to bytes on a UTF-8 sink (net8+), else ISpanFormattable into a +// stackalloc char span, else ToString(format, provider). Identical characters on every tier. +scope.Renderer.Render(count, "N0", CultureInfo.InvariantCulture); // where count : struct, ISpanFormattable +``` + +The capability interfaces behind this are additive and opt‑in — you never have to implement them: + +| Interface | Adds | Implemented by | +| --- | --- | --- | +| `IScopeRenderer` | `Render(string)` | every renderer (unchanged) | +| `ISpanScopeRenderer : IScopeRenderer` | `Render(ReadOnlySpan)` | the sink adapters and `HtmlEncodedRenderer` | +| `IUtf8ScopeRenderer : ISpanScopeRenderer` | `RenderUtf8(ReadOnlySpan)` | the UTF‑8 sink only | + +`HtmlEncodedRenderer` is deliberately **not** an `IUtf8ScopeRenderer`: pre‑encoded bytes must never +skip an active encode proxy, so under `@html` a value routes through the string bridge (encode, +then transcode) and is single‑encoded by construction. + +> **Never cache the renderer.** `scope.Renderer` is a **per‑render** artifact — the string path +> builds a fresh one each call, and the sink overloads construct a new adapter per render. Read it +> from the `Scope` you were handed; never store it in a field. Extension instances are shared across +> concurrent renders and must stay stateless (all per‑render state lives in the `Scope` lineage). + +--- + +## The local context channel + +Sibling extensions can coordinate declaratively through a small per‑body **local context frame** +reached from `Scope`: + +```csharp +scope.Publish(string key, object value); // last write wins within the frame +bool scope.TryRead(string key, out object value); // false (never throws) when absent +``` + +This is the general mechanism behind the built‑in [branch sets](built-in-extensions.md#branch-sets): +`@if`/`@ifnot`/`@elif` publish a `BranchState` and `@else` reads and clears it. Use it for any +"publish in document order, read by a later sibling" pattern — zebra striping, tab sets, +first‑match‑wins pickers. + +**Rules of the road:** + +- **Declare participation with `[ScopeChannel]`.** A body is provisioned with a frame at compile + time **iff** its document statically contains a `[ScopeChannel]` extension. Mark every extension + that calls `Publish`/`TryRead` with `[ScopeChannel]` (it is inherited by subclasses). Forget it + and `Publish` throws `InvalidOperationException`; `TryRead` returns `false`. This keeps templates + that use no channel allocation‑identical — they never provision a frame. +- **Keys are ordinal, case‑sensitive strings; `null` values are allowed.** The prefix `heddle.` is + reserved for the engine — only `BranchState.ReservedKey` (`"heddle.branch"`) may be published + under it, and only with a `BranchState` value. Namespace your own keys (e.g. `"myapp.row"`). +- **Frames never cross a body boundary.** Each `@list`/`@for` iteration, nested body, `@partial`, + and definition invocation starts a fresh frame; a body never sees its parent's. Coordination is + strictly across **siblings** of one body execution (like CSS counters), never parent→child. +- **Thread‑safety.** A frame belongs to one body execution of one render invocation on one thread, + so `Publish`/`TryRead` are unsynchronized by design. As always, never store a `Scope` on your + extension instance or use it after the call that received it — extension instances are shared + across concurrent renders. +- **Bodies execute only through the funnel.** Any extension that runs a subtemplate body does so + via the protected `GetInnerResult(in Scope)` / `RenderInnerResult(in Scope)` — the single seam + that installs the body's frame. Custom extensions inherit this for free. + +### Example — a publisher/consumer pair (zebra striping) + +One `[ScopeChannel]` extension both reads the previous row parity and publishes the next, so a run +of siblings alternates: + +```csharp +[ExtensionName("zebra")] +[ScopeChannel] +public class ZebraExtension : AbstractExtension +{ + private const string Key = "myapp.zebra.row"; + + public override object ProcessData(in Scope scope) => Next(scope); + public override void RenderData(in Scope scope) => scope.Renderer.Render(Next(scope)); + + private static string Next(in Scope scope) + { + bool odd = scope.TryRead(Key, out var value) && value is bool b && b; + scope.Publish(Key, !odd); // flip for the next sibling + return odd ? "odd" : "even"; + } +} +``` + +`@zebra()@zebra()@zebra()` renders `evenoddeven`; inside a `@list` body each row starts fresh. + +### Example — a `BranchState` participant that drives a set + +`BranchState` and its reserved key are public, so a custom matcher can **satisfy** a branch set — +publishing `new BranchState(true)` makes a following `@else` render nothing: + +```csharp +[ExtensionName("satisfy")] +[ScopeChannel] +public class SatisfyExtension : AbstractExtension +{ + public override object ProcessData(in Scope scope) + { + scope.Publish(BranchState.ReservedKey, new BranchState(true)); + return string.Empty; + } + + public override void RenderData(in Scope scope) + { + scope.Publish(BranchState.ReservedKey, new BranchState(true)); + } +} +``` + +`@satisfy()@else(){{ fallback }}` renders nothing — the set is already satisfied. A matcher could +just as well read the state with `scope.TryRead(BranchState.ReservedKey, out var v)` to render +alongside a set. + --- ## A minimal example @@ -152,12 +287,12 @@ Declared in [src/Heddle/Attributes](../src/Heddle/Attributes): | Attribute | Target | Purpose | | --- | --- | --- | -| `[ExtensionName("name")]` | class | The verb used in templates (`@name(...)`). Required. The empty name `""` is reserved for the unnamed `@(...)` extension. | +| `[ExtensionName("name")]` | class | The verb used in templates (`@name(...)`). Required. The empty name `""` is the unnamed `@(...)` carrier; `raw` is its always‑verbatim alias. | | `[DataType(typeof(T))]` | class | The model type the extension expects. Repeatable (e.g. `int` *and* `long` on `IntegerExtension`). | | `[ChainedType(typeof(T))]` | class | The expected chained‑input type. | -| `[EncodeOutput]` | class | HTML‑encode the output by default (pairs with `AbstractHtmlExtension`). | +| `[EncodeOutput]` | class | HTML‑encode the output (pairs with `AbstractHtmlExtension`). This encodes under **both** output profiles — it is independent of `OutputProfile`, which only governs the unnamed `@(...)` carrier. Keep `[EncodeOutput]` on value formatters that emit user text; leave it off for containers that merely forward a body (so encoding stays at the emitting leaf). | | `[ExtensionReplace]` | class | Marks an extension intended to replace another of the same name. | -| `[NotEncode]` | model property | Mark a *model* property as pre‑trusted so its value isn't HTML‑encoded. | +| `[NotEncode]` | model property | Reserved, currently **inert** — the attribute type ships but has no effect (its only check runs against extension classes, never properties). Do not rely on it; its per‑property meaning is revisited with typed props. | | `[Hidden]` | model property | Hide a model property from template resolution. | | `[Options("fieldName")]` | member | Override the name a property is addressed by in templates. | @@ -192,6 +327,75 @@ Two steps: `Configure` walks the given assembly and its references, so exporting from any referenced assembly is sufficient as long as that assembly is reachable from the one you pass. +## Precompiled mode + +When you [pre‑compile templates](precompilation.md) at build time, a custom extension is +**bound from its referenced assembly, never inlined** — a security or logic patch reaches +precompiled templates by updating the package, no regeneration. For a template that uses your +extension to precompile, the extension must satisfy the same contract precompiled binding +reproduces: + +- **A parameterless constructor.** The generator constructs one shared, pre‑built instance per + call site (`new YourExtension()`); no `Activator`, no registry lookup at run time. +- **No reliance on runtime registry mutation.** The instance is built once and never mutated + after binding; extensions that expect to be re‑registered or reconfigured per render are not + supported. +- **No `InitStart`/`CompleteInit` override** *(outside the engine assembly)*. Those are + compile‑time hooks the build‑time backend runs the *base* behavior of; an override could run + arbitrary compile‑time logic the generator cannot evaluate, so a template binding such an + extension is a build error (`HED7015`). Keep custom logic in `ProcessData`/`RenderData` — the + render‑time methods both backends share. A plain extension (the common case) needs no changes. + +An extension name that resolves to no `[ExtensionName]` type in any referenced assembly is a +build error (`HED7006`). Extensions that only ever run through the dynamic path are unaffected. + +## Declaratively exporting functions + +Registered functions (the native‑expression helpers of +[native expressions](native-expressions.md)) can also be exported **declaratively** from an +assembly, so the same set is visible to the host at runtime, to the editor tooling, and — in a +build‑time compilation — to the source generator. One attribute, three readers. + +1. **Export** the function container(s) with the assembly‑level attribute + [`ExportFunctions`](../src/Heddle/Attributes/ExportFunctionsAttribute.cs). A container is a + `public static` class; **every** public static method it declares becomes one registrable + function under its lowercase‑invariant method name (`TitleCase` → `titlecase`). Make any helper + that must not be exported non‑public — the container is the unit of export. + + ```csharp + using Heddle.Attributes; + + [assembly: ExportFunctions(typeof(MyApp.TemplateFunctions))] + // or several: [assembly: ExportFunctions(typeof(A), typeof(B))] + + namespace MyApp + { + public static class TemplateFunctions + { + public static string TitleCase(string value) => /* … */; // → titlecase(string) + } + } + ``` + + Eligibility is exactly the [`FunctionRegistry.Register`](csharp-api.md) rule set: static, closed + (no open generics), non‑`void`, no `ref`/`out`/pointer parameters. An ineligible method or a + non‑public/non‑static container is a host programming error (`ArgumentException`). + +2. **Register** the exports into your function registry at startup — before the first compile: + + ```csharp + var functions = new FunctionRegistry(); // starts with the built‑ins + functions.RegisterFrom(typeof(Program).Assembly); // add every [ExportFunctions] export + var options = new TemplateOptions { Functions = functions }; + ``` + +`RegisterFrom` goes through the exact `Register(string, MethodInfo)` path — replace on an exact +signature, overload otherwise, the same ranked overload resolution. Calling it during startup keeps +the runtime registry identical to what the editor's one‑shot workspace scan sees, so completion, +hover, and diagnostics match your host's compile. Purely runtime `Register(name, delegate)` +registrations remain host‑only (invisible to the editor) — export them declaratively to share them. +See [editor support](editor-support.md) for the editor side. + --- ## Common categories in practice diff --git a/docs/demo-smoke/demo.spec.ts b/docs/demo-smoke/demo.spec.ts new file mode 100644 index 00000000..9a2c2bc0 --- /dev/null +++ b/docs/demo-smoke/demo.spec.ts @@ -0,0 +1,99 @@ +import { test, expect, Page } from '@playwright/test'; + +// Phase 9 D10 — the demo smoke suite (chromium only). It is the browser half of the cross-host contract +// (S3/S4 assert the same fixtures DemoContractTests asserts on CoreCLR) and the docs workflow's red/green gate +// before deploy. S1 is the standing fallback regression (today's behavior byte-for-byte when WASM is blocked). + +const DEMO = 'demo.html'; + +// Wait until the status pill reaches (at least) the given layer. +async function waitForLayer(page: Page, label: 'Typed' | 'Typed + Render') { + await expect(page.locator('#status-pill')).toHaveText(label, { timeout: 45_000 }); +} + +test.describe('Heddle demo', () => { + test('S1 fallback: WASM blocked -> syntax layer still works', async ({ page }) => { + const pageErrors: string[] = []; + page.on('pageerror', (e) => pageErrors.push(String(e))); + + // Abort every request to the demo bundle before navigating: the typed layer can never boot. + await page.route('**/demo/**', (r) => r.abort()); + await page.goto(DEMO); + + await expect(page.locator('#editor')).toBeVisible(); + await expect(page.locator('#status-pill')).toHaveText('Syntax'); + + // Delete a ')' from the starter template to provoke a parse-error annotation (base-layer behavior). + const editor = page.locator('#editor'); + await editor.click(); + await page.keyboard.type('@(Name'); // an unclosed call -> the mode worker flags it + // The Ace gutter shows an error annotation. + await expect(page.locator('.ace_gutter-cell.ace_error, .ace_error')).toHaveCount(1, { timeout: 15_000 }); + + expect(pageErrors, 'no uncaught page errors in fallback mode').toEqual([]); + }); + + test('S2 typed diagnostics: a member typo carries a HED code', async ({ page }) => { + await page.goto(DEMO); + await waitForLayer(page, 'Typed'); + + // Replace a valid member with a typo inside an @(...) expression. + await page.locator('#editor').click(); + await page.keyboard.press('Control+A'); + await page.keyboard.type('@using(){{Heddle.Demo.Models}}\n@model(){{Blog}}\n@(Titlle)\n'); + + // An annotation whose text carries a HED code appears. + const annotation = page.locator('.ace_gutter-cell.ace_error'); + await expect(annotation).toHaveCount(1, { timeout: 30_000 }); + }); + + test('S3 typed completion: Article members complete (C01 fixture, browser side)', async ({ page }) => { + await page.goto(DEMO); + await waitForLayer(page, 'Typed'); + + await page.locator('#editor').click(); + // Set the template and place the caret right after "@(" inside the @list(Articles) body (Article scope) via the + // Ace API: keyboard.type leaves the caret at the line end (outside the expression) and keystroke auto-pairing is + // editor-version fragile. column 20 is immediately after "@(" on the third line. + await page.evaluate(() => { + const ed = (window as any).ace.edit('editor'); + ed.setValue('@using(){{Heddle.Demo.Models}}\n@model(){{Blog}}\n@list(Articles){{ @( }}', -1); + ed.moveCursorTo(2, 20); + ed.focus(); + }); + await page.keyboard.press('Control+Space'); + + const popup = page.locator('.ace_autocomplete'); + await expect(popup).toBeVisible({ timeout: 30_000 }); + // Ace's autocomplete is a virtualized list that only renders the top rows into the DOM; the items sort + // alphabetically, so assert on Article-only members that land in that rendered window (Author, Comments) — + // neither exists on the Blog root, so they still prove the completion is scoped to the Article list body. + await expect(popup).toContainText('Author'); + await expect(popup).toContainText('Comments'); + }); + + test('S4 render round-trip: the blog starter renders the pinned fragment', async ({ page }) => { + await page.goto(DEMO); + await waitForLayer(page, 'Typed + Render'); + + const frame = page.frameLocator('#render-frame'); + await expect(frame.locator('h1')).toContainText('Heddle Weekly', { timeout: 30_000 }); + await expect(frame.locator('.badge')).toContainText('Featured'); + }); + + test('S5 C# tier declined: a C#-tier construct is refused, not crashed', async ({ page }) => { + const pageErrors: string[] = []; + page.on('pageerror', (e) => pageErrors.push(String(e))); + + await page.goto(DEMO); + await waitForLayer(page, 'Typed'); + + await page.locator('#editor').click(); + await page.keyboard.press('Control+A'); + // A method call is a native-tier rejection (HED1003) — the browser demo's "declined gracefully" surface. + await page.keyboard.type('@using(){{Heddle.Demo.Models}}\n@model(){{Blog}}\n@(Title.ToUpper())\n'); + + await expect(page.locator('.ace_gutter-cell.ace_error')).toHaveCount(1, { timeout: 30_000 }); + expect(pageErrors, 'no uncaught page errors when a construct is declined').toEqual([]); + }); +}); diff --git a/docs/editor-support.md b/docs/editor-support.md new file mode 100644 index 00000000..de2c4be9 --- /dev/null +++ b/docs/editor-support.md @@ -0,0 +1,127 @@ +# Editor support + +Heddle ships a Language Server Protocol implementation and a VS Code extension that give `.heddle` +authors **typed** member completion, live diagnostics at template positions, hover types, semantic +coloring, and go‑to‑definition across imports. Because the compiler threads real type information +through every scope, completion is *correct*, not heuristic — and per‑use‑site type errors in +abstract definitions surface at the exact position that caused them. + +Everything is a projection of the real engine pipeline: the server contains no compiler logic of its +own. The same [`Heddle.LanguageServices`](csharp-api.md) facade that powers the LSP is a public +package you can host yourself. + +## Installing + +### VS Code + +Install the **Heddle Template Language** extension from the marketplace. It bundles the language +server, so no separate install is needed. The extension keeps working (TextMate coloring) even when +the server is unavailable. + +### The dotnet tool (any editor) + +The server is a .NET tool, `heddle-lsp`. Install it version‑pinned: + +``` +dotnet tool install --global Heddle.LanguageServer --version +``` + +or, recommended for teams, as a repo‑local tool so the pin lives in source control: + +``` +dotnet new tool-manifest +dotnet tool install Heddle.LanguageServer --version +``` + +The tool targets .NET 10 and requires the .NET 10 runtime. Distribution is **explicit‑install only** — +the extension never downloads and executes the server from a feed at launch. + +### Neovim (or any generic LSP client) + +Point your client at the `heddle-lsp` executable over stdio. For Neovim `lspconfig`: + +```lua +require('lspconfig.configs').heddle = { + default_config = { + cmd = { 'heddle-lsp' }, + filetypes = { 'heddle' }, + root_dir = require('lspconfig.util').root_pattern('.heddle-lsp.json', '.git'), + }, +} +require('lspconfig').heddle.setup {} +``` + +## Configuring + +Two configuration channels; a **`.heddle-lsp.json`** at the workspace root wins field‑by‑field over +client settings, so it is the editor‑agnostic carrier. + +```json +{ + "assemblies": ["bin/Debug/net10.0/MyApp.dll"], + "rootPath": "Views", + "outputProfile": "html", + "expressionMode": "native", + "fileNamePostfix": ".heddle" +} +``` + +| Field | Meaning | +| --- | --- | +| `assemblies` | Model assemblies for typed completion/hover, **and** the input of the one‑shot export scan (see below). Relative to the workspace root. | +| `rootPath` | Template root for `@<<` import and `@partial` resolution (`TemplateOptions.RootPath`). | +| `outputProfile` | `text` or `html` — so diagnostics match your host's compile options. | +| `expressionMode` | `memberPathsOnly` / `native` / `fullCSharp`. | +| `fileNamePostfix` | Template file name postfix. | + +The VS Code extension contributes mirror settings (`heddle.model.assemblies`, +`heddle.workspace.rootPath`, `heddle.compile.outputProfile`, `heddle.compile.expressionMode`, +`heddle.compile.fileNamePostfix`, plus `heddle.server.path` and `heddle.trace.server`) and forwards +them to the server. + +**Types are stale until rebuild.** The editor loads your model assemblies as they are on disk; +rebuild your project to pick up type changes. + +## Host registrations reach the editor via one scan + +The server process never runs your host's startup code, so functions and extensions you register at +runtime are invisible to it *unless they are declared in the assembly*. At workspace load the editor +runs a **one‑shot scan** of the configured `assemblies`: + +- Assembly‑level [`[ExportExtensions]`](custom-extensions.md#registering-your-extensions) — the + exported extensions become offerable names and their calls stop drawing "unknown extension". +- Assembly‑level [`[ExportFunctions]`](custom-extensions.md#declaratively-exporting-functions) — the + exported functions register into the workspace registry, so their calls resolve (no false + "unknown function"), complete with real signatures, and participate in expression typing. + +For runtime parity, call +[`FunctionRegistry.RegisterFrom(assembly)`](custom-extensions.md#declaratively-exporting-functions) +on the same assemblies in your host startup — the editor and the host then see one set. + +**The scan is one‑shot per server process.** A new export, a changed extension body, or an +`assemblies` change after load requires a **server restart** (VS Code: *Heddle: Restart Language +Server*). Consider a dedicated export assembly so an extension/function export never pulls model +types into two load contexts. + +**Delegate‑only registrations stay host‑only.** A purely runtime `Register(name, delegate)` closure +cannot be discovered by scanning metadata; its calls draw an editor‑only "unknown function" even +though your host resolves them. Export the function declaratively to share it. + +## No‑server fallback + +VS Code applies semantic tokens *on top of* the TextMate grammar, so when the server is down or not +installed the extension still colors `.heddle` files from the grammar — no configuration needed. + +## Troubleshooting + +| Symptom | Cause / fix | +| --- | --- | +| "language server not found" | Install the tool (`dotnet tool install --global Heddle.LanguageServer --version `) or set `heddle.server.path`. | +| "the .NET 10 runtime was not found" | Install the .NET 10 runtime. | +| Types don't complete | No `assemblies` configured, or the project was not rebuilt. Check the paths in `.heddle-lsp.json`. | +| A definition/prop shows stale types | Rebuild — model types update on rebuild, not on source edit. | +| An extension or function is not offered | The export attribute is missing, the method is not an eligible public static, or the server needs a restart to rescan. Exported names are the **lowercase** method names; lookup is ordinal and case‑sensitive. | + +See also: [syntax highlighting](syntax-highlighting.md) (grammar‑only setups), +[getting started](getting-started.md), and [the C# API](csharp-api.md) (hosting the facade +programmatically). diff --git a/docs/language-reference.md b/docs/language-reference.md index 3a9f0457..5e0d518b 100644 --- a/docs/language-reference.md +++ b/docs/language-reference.md @@ -17,23 +17,26 @@ runnable templates that exercise edge cases, see the test fixtures in 2. [Symbol cheat sheet](#symbol-cheat-sheet) 3. [Text and the `@` escape](#text-and-the--escape) 4. [Output blocks](#output-blocks) -5. [Member expressions `@(A.B.C)`](#member-expressions-abc) -6. [Root reference `@(::Member)`](#root-reference-member) -7. [Context and data flow](#context-and-data-flow) -8. [Embedded C# expressions](#embedded-c-expressions) -9. [Definitions `@% … %@`](#definitions---) -10. [Default output `-> chain`](#default-output---chain) -11. [Type annotation `:: Type`](#type-annotation--type) -12. [Inheritance and override ``](#inheritance-and-override-childbase) -13. [Subtemplates `{{ … }}`](#subtemplates--) -14. [Chaining with `:`](#chaining-with-) -15. [Recursion](#recursion) -16. [Imports `@<<{{ … }}`](#imports--) -17. [Comments `@* … *@`](#comments---) -18. [Raw blocks `@{ … }@` and `@:`](#raw-blocks----and-) -19. [Whitespace trimming `@\`](#whitespace-trimming-) -20. [How the lexer reads a template (modes)](#how-the-lexer-reads-a-template-modes) -21. [Behavioral nuances summary](#behavioral-nuances-summary) +5. [Output profiles](#output-profiles) +6. [Member expressions `@(A.B.C)`](#member-expressions-abc) +7. [Root reference `@(::Member)`](#root-reference-member) +8. [Context and data flow](#context-and-data-flow) +9. [Embedded C# expressions](#embedded-c-expressions) +10. [Definitions `@% … %@`](#definitions---) +11. [Default output `-> chain`](#default-output---chain) +12. [Type annotation `:: Type`](#type-annotation--type) +13. [Props ``](#props-nameprop-type--default) +14. [Parameterized slots `out:: Type`](#parameterized-slots-out-type) +15. [Inheritance and override ``](#inheritance-and-override-childbase) +16. [Subtemplates `{{ … }}`](#subtemplates--) +17. [Chaining with `:`](#chaining-with-) +18. [Recursion](#recursion) +19. [Imports `@<<{{ … }}`](#imports--) +20. [Comments `@* … *@`](#comments---) +21. [Raw blocks `@{ … }@` and `@:`](#raw-blocks----and-) +22. [Whitespace trimming `@\`](#whitespace-trimming-) +23. [How the lexer reads a template (modes)](#how-the-lexer-reads-a-template-modes) +24. [Behavioral nuances summary](#behavioral-nuances-summary) --- @@ -110,6 +113,9 @@ deliberate, and both are what make templates compose: | `@% … %@` | Definition block | Declare one or more named templates. | | `` | Definition name | Names a template inside `@% … %@`. | | `` | Inheritance | Define `child` based on `base`. | +| `` | Props | Typed named parameters with literal defaults; passed by name (`@name(model, p: v)`). | +| `out:: Type` | Slot parameter | Declares a typed slot; `@out(expr)` renders the caller body once per value as its model. | +| `this` | Current model | The current scope's model as an expression (e.g. `@out(this)`). | | `-> chain` | Default output | Marks a definition to render automatically; `chain` selects its data. | | `:: Type` | Type annotation | Strongly types a definition's model. | | `::Member` | Root reference | Read `Member` from the root model, not the current one. | @@ -201,12 +207,48 @@ with a body it renders the body against its model; with no body it just stringif --- +## Output profiles + +Whether the bare `@(value)` output HTML‑encodes is governed by the **output profile**, not by +the syntax. There are two: + +- **`OutputProfile.Text`** — the 1.x default. `@(value)` emits raw text (text/JSON/code + generation). Every existing template renders byte‑identically. +- **`OutputProfile.Html`** — a bodiless `@(value)` HTML‑encodes by default, closing the + XSS‑by‑default gap. `@raw(value)` opts a trusted value out; a bodied `@(value){{…}}` remains a + raw rescoping container (its literal body markup is never encoded — only value leaves inside). + +```heddle +@profile(){{ html }} +

    @(UserInput)

    @* - + ``` -**Why:** the unnamed `@(...)` does not HTML‑encode, so a serialized JSON string is written -as‑is. Only do this with values **you** produced — never with untrusted text. See -[HTML encoding](built-in-extensions.md#html-encoding). +**Why:** a serialized JSON string must be written as‑is. Under `OutputProfile.Text` the bare +`@(ArticleJson)` already emits raw, but under an `OutputProfile.Html` host the bare form +HTML‑encodes — so use `@raw(...)` (or run this template under `Text`) to guarantee verbatim +output regardless of the host's profile. Only do this with values **you** produced — never +with untrusted text. See [Output profiles](built-in-extensions.md#output-profiles). --- @@ -244,15 +313,31 @@ as‑is. Only do this with values **you** produced — never with untrusted text **Problem:** decide where whitespace trimming actually matters. +**Best option — turn on `TrimDirectiveLines` and drop the `@\` boilerplate.** When a whole line +is nothing but a no‑output directive, the option swallows the line for you: + +```csharp +var options = new TemplateOptions { TrimDirectiveLines = true }; +``` + +```heddle +@using(){{System.Linq}} @* whole line swallowed — no @\ needed *@ +@model(){{PageContext}} +@* preamble comment — its line goes too *@ +
    @(Title)
    +``` + +If you can't enable the option (or need **mid‑line** control), `@\` still works exactly as before: + ```heddle -@using(){{System.Linq}}@\ @* trim: keeps the preamble from emitting blank lines *@ +@using(){{System.Linq}}@\ @* @\: trims the following whitespace including the newline *@ @model(){{PageContext}}@\
    @(Title)
    @* HTML: no @\ needed — the browser ignores the whitespace *@ ``` **Why:** Heddle is whitespace‑significant, but HTML collapses insignificant whitespace, so most -markup doesn't need `@\`. Reserve it for the declaration preamble and for whitespace‑sensitive -output (plain text, `
    `, JSON). See
    +markup doesn't need `@\`. Reserve `@\` for mid‑line control; use `TrimDirectiveLines` for the
    +whole‑line preamble noise. See
     [Whitespace trimming](language-reference.md#whitespace-trimming-).
     
     ---
    diff --git a/docs/playwright.config.ts b/docs/playwright.config.ts
    new file mode 100644
    index 00000000..9b1a363e
    --- /dev/null
    +++ b/docs/playwright.config.ts
    @@ -0,0 +1,24 @@
    +import { defineConfig, devices } from '@playwright/test';
    +
    +// Phase 9 D10 — the demo smoke suite runs against the BUILT site (VitePress preview with the /Heddle/ base),
    +// not the dev server, so the deploy shape (base path, staged demo/ bundle) is exercised. Chromium only.
    +export default defineConfig({
    +  testDir: './demo-smoke',
    +  timeout: 60_000,
    +  expect: { timeout: 15_000 },
    +  fullyParallel: false,
    +  retries: process.env.CI ? 1 : 0,
    +  reporter: 'list',
    +  webServer: {
    +    command: 'npm run docs:preview -- --port 4173 --host 127.0.0.1',
    +    url: 'http://127.0.0.1:4173/Heddle/',
    +    timeout: 120_000,
    +    reuseExistingServer: !process.env.CI,
    +  },
    +  use: {
    +    baseURL: 'http://127.0.0.1:4173/Heddle/',
    +  },
    +  projects: [
    +    { name: 'chromium', use: { ...devices['Desktop Chrome'] } },
    +  ],
    +});
    diff --git a/docs/precompilation.md b/docs/precompilation.md
    new file mode 100644
    index 00000000..894087bb
    --- /dev/null
    +++ b/docs/precompilation.md
    @@ -0,0 +1,216 @@
    +# Build‑Time Pre‑compilation
    +
    +Heddle can pre‑compile your `.heddle` templates **at build time** into your application
    +assembly — the Razor compiled‑views shape. A precompiled template is **looked up, not parsed
    +and compiled** at run time: no ANTLR parse, no expression trees, no Roslyn work at startup,
    +and when every template an app renders is precompiled `Microsoft.CodeAnalysis` never even
    +loads. Templates are validated by the build (a broken template fails compilation at its
    +`.heddle` position) and become debuggable (real generated C#, real stack traces).
    +
    +Pre‑compilation is **purely additive and opt‑in**. The runtime stays fully dynamic:
    +runtime‑loaded template strings, `:: dynamic`, `@AllowCSharp` runtime compilation, and hot
    +reload all keep working, and precompiled and runtime‑compiled templates coexist in one
    +process. The precompiled assembly is a *cache seeded at build time*, never a cage.
    +
    +> The generated backend must produce **byte‑identical** output to the runtime backend — a
    +> differential harness proves it across the whole fixture corpus on every build. The runtime
    +> path is the semantic reference.
    +
    +---
    +
    +## Setup
    +
    +Add the generator package (it ships the emitter in `analyzers/dotnet/cs`; the core `Heddle`
    +package stays runtime‑only and unrestricted):
    +
    +```xml
    +
    +  
    +  
    +
    +```
    +
    +By default every `**/*.heddle` file in the project (excluding `bin`/`obj`) is picked up as a
    +template. Opt individual files out, or add extra ones, with the `HeddleTemplate` item:
    +
    +```xml
    +
    +  
    +  
    +  
    +  
    +  
    +  
    +
    +
    +  false
    +
    +```
    +
    +## Compile options (MSBuild properties)
    +
    +These mirror `TemplateOptions` and are baked into the generated artifact; a mismatch against
    +the runtime request is caught by the validation gauntlet (below). An unparsable value is a
    +build error (`HED7009`).
    +
    +| Property | Values / default | Effect |
    +| --- | --- | --- |
    +| `HeddleOutputProfile` | `Text` (default) \| `Html` | Output encoding profile (part of the options fingerprint). |
    +| `HeddleExpressionMode` | `MemberPathsOnly` \| `Native` (default) \| `FullCSharp` | Expression tier. `FullCSharp` is the build‑time equivalent of `AllowCSharp`. |
    +| `HeddleTrimDirectiveLines` | `false` (default) \| `true` | Trim directive‑only lines (part of the fingerprint). |
    +| `HeddleMaxRecursionCount` | positive int, default `100` | Definition‑carrier recursion limit, baked at build. |
    +| `HeddleTemplateRoot` | dir, default `$(MSBuildProjectDirectory)` | Root the template key is made relative to. |
    +| `HeddleGeneratedNamespace` | default `$(RootNamespace).HeddleTemplates` | Namespace of the generated entry classes. |
    +| `HeddleEmitUtf8Pieces` | `false` (default) \| `true` | Emit pre‑encoded `"…"u8` static pieces for the byte sink. |
    +
    +---
    +
    +## Typed entry points — the recommended host API
    +
    +Each precompiled template emits a static class (`{HeddleGeneratedNamespace}.{SanitizedName}`,
    +e.g. `views/home/index.heddle` → `Views_Home_Index`). Call it directly — compile‑checked
    +model type, no lookup, no validation gauntlet:
    +
    +```csharp
    +string html = MyApp.HeddleTemplates.Views_Home_Index.Generate(model);
    +```
    +
    +The signature is `Generate(TModel model, object chained = null, object callerData = null)`
    +where `TModel` is the declared `@model` type (`object` for `:: dynamic`, none for a
    +model‑less template).
    +
    +## The registry — for dynamic call sites
    +
    +Templates identified by a runtime value (a path, a database key) resolve through the registry
    +instead. Registration is repeatable and idempotent per assembly:
    +
    +```csharp
    +using Heddle.Precompiled;
    +
    +PrecompiledTemplates.Register(typeof(MyApp.Program).Assembly);   // once per assembly
    +// (HeddleTemplate.Configure(assembly) also registers, for convenience)
    +
    +// Discovery is a first-class, public API — keys, model types, fingerprints, capabilities:
    +foreach (var entry in PrecompiledTemplates.Entries)
    +    Console.WriteLine($"{entry.Key}  model={entry.ModelType}  precompiled={entry.IsPrecompiled}");
    +```
    +
    +`TemplateResolver.GetTemplate` consults the registry **before** the dynamic cache and file
    +check. On a hit it runs the per‑request validation gauntlet; all pass → a `HeddleTemplate` in
    +precompiled‑adapter mode (zero parse, zero compile). A registry **miss** is never a failure —
    +the dynamic path proceeds untouched.
    +
    +## The validation gauntlet and mismatch policy
    +
    +Before trusting a precompiled entry the resolver checks it is compatible with the request:
    +
    +- **Options fingerprint** — `(OutputProfile, ExpressionMode, TrimDirectiveLines)`. A template
    +  compiled under `Text` is not a valid answer for an `Html` request.
    +- **Extension bindings** — the `[ExtensionName]` extensions the template bound at build vs.
    +  the live registry (catches `[ExtensionReplace]` overrides). Default match is
    +  assembly‑qualified type name *without* version; supply your own via
    +  `PrecompiledTemplates.BindingResolver`.
    +- **Function bindings** — the functions the template called vs. the request's effective
    +  `FunctionRegistry` (below).
    +- **Staleness** — only under `EnableFileChangeCheck`: the root file's `ContentHash` and the
    +  transitive `@<<` import closure vs. disk.
    +
    +On any failure, behavior is controlled by `TemplateOptions.PrecompiledMismatchPolicy`:
    +
    +- **`Fallback` (default)** — take the unchanged dynamic path and recompile, plus one
    +  `PrecompiledTemplates.OnFallback` callback and an `HED7101` warning naming the reason. A
    +  silent fallback that quietly re‑adds compile cost is the failure mode this design guards
    +  against.
    +- **`Strict`** — an entry that *exists* but fails the gauntlet **throws**
    +  `PrecompiledMismatchException` (for deployments that must never pay dynamic‑compile cost). A
    +  plain registry **miss** is not a `Strict` failure — strictness polices divergence, not
    +  coverage.
    +
    +An assembly whose manifest schema/engine version is incompatible is ignored wholesale (every
    +template falls back), with one `HED7102` callback per manifest.
    +
    +---
    +
    +## Functions in precompiled templates
    +
    +Built‑in functions bind through a public shim; **host functions must be discoverable at build
    +time**, which means exported declaratively from a *referenced* assembly:
    +
    +```csharp
    +[assembly: Heddle.Attributes.ExportFunctions(typeof(Acme.Web.TemplateFunctions))]
    +
    +namespace Acme.Web
    +{
    +    public static class TemplateFunctions
    +    {
    +        public static string TitleCase(string s) => /* … */;   // -> @(titlecase(x))
    +    }
    +}
    +```
    +
    +The build binds `@(titlecase(x))` **directly** to `Acme.Web.TemplateFunctions.TitleCase` — no
    +registry dispatch, a real stack frame, and a logic fix in the function package reaches
    +precompiled templates by updating the reference. **Parity rule:** the build must reference the
    +assemblies the host registers from — every `options.Functions.RegisterFrom(assembly)` in
    +startup pairs with a build‑time reference to the same assembly; both read the same
    +`[ExportFunctions]` metadata.
    +
    +A function that *cannot* be expressed as an exported `public static` method (a
    +`Register(string, Delegate)` closure) is not representable in assembly metadata: the template
    +draws build warning `HED7014` and is left un‑precompiled (a fallback‑marker entry) — it
    +compiles dynamically at run time. Wrap the closure in an exported static method to precompile
    +it.
    +
    +## Custom extensions in precompiled templates
    +
    +Custom `[ExtensionName]` extensions bind **from the referenced assembly, never inlined** — a
    +security or logic patch reaches precompiled templates by updating the package. See
    +[custom‑extensions.md](custom-extensions.md#precompiled-mode) for the requirements
    +(parameterless ctor; no reliance on runtime registry mutation; a `InitStart`/`CompleteInit`
    +override is refused as `HED7015`).
    +
    +---
    +
    +## Build‑time diagnostics
    +
    +Every build‑time condition reports at its `.heddle` position with an `HED7xxx` id:
    +
    +| Id | Meaning |
    +| --- | --- |
    +| `HED7001` | An `AdditionalFiles` `.heddle` source could not be read. |
    +| `HED7002` | Two templates normalize to the same key. |
    +| `HED7003` | Two keys differ only by case (warning). |
    +| `HED7004` | Invalid explicit `Key` metadata. |
    +| `HED7005` | Unpaired surrogate in static text — the `"…"u8` twin is suppressed (warning). |
    +| `HED7006` | A named extension resolves to no `[ExtensionName]` type in any reference. |
    +| `HED7007` | The `@model`/`::` type does not resolve (milestone‑2 native diagnostic). |
    +| `HED7008` | A member path does not resolve on the model type (milestone‑2 native diagnostic). |
    +| `HED7009` | An MSBuild option value is unparsable. |
    +| `HED7010` | Two keys sanitize to one generated class identifier. |
    +| `HED7011` | An `@<<` import is not among the compilation's `.heddle` `AdditionalFiles`. |
    +| `HED7012`/`HED7013` | A forwarded front‑end error/warning carrying no id. |
    +| `HED7014` | A called function is delegate‑only (not precompilable) — the template falls back (warning). |
    +| `HED7015` | A bound extension overrides a compile‑time hook — unevaluable at build. |
    +
    +Member/type errors in milestone 1 arrive as C# errors remapped to the template span via
    +`#line`; milestone 2 replaces the covered ones with native `HED7007`/`HED7008`.
    +
    +---
    +
    +## The T4‑successor CLI
    +
    +The `heddle` CLI hosts the full dynamic engine — the T4‑successor codegen story. Installed as a
    +`dotnet tool`, it renders a template against a JSON model:
    +
    +```bash
    +dotnet tool install --global Heddle.Tool
    +heddle render Templates/Enum.heddle --model-json color.json --out Color.cs
    +```
    +
    +Invoked from an MSBuild `` step it turns data into source; the produced artifact carries
    +the generated code but **no runtime Heddle dependency**. `color.json` objects become member
    +access, arrays drive `@list`/`@for`, and scalars map to CLR primitives.
    +
    +```
    +heddle render