From 303c09188743041be292d1b5af46f1685385da4f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 6 Oct 2025 19:43:50 +0000 Subject: [PATCH 1/5] Initial plan From d37f1756aa3dc14cca789b204ab9f513bbe3a83c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 6 Oct 2025 19:58:01 +0000 Subject: [PATCH 2/5] Add support for new import syntax {{#import:}} and deprecate @include/@import Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/cli/imports.go | 14 +- pkg/cli/remove_command.go | 7 +- pkg/parser/frontmatter.go | 72 +++++- pkg/parser/import_syntax_test.go | 223 ++++++++++++++++++ .../compiler_template_validation_test.go | 2 +- pkg/workflow/template.go | 17 +- .../template_include_validation_test.go | 18 +- 7 files changed, 314 insertions(+), 39 deletions(-) create mode 100644 pkg/parser/import_syntax_test.go diff --git a/pkg/cli/imports.go b/pkg/cli/imports.go index b8f965a0e30..5aad55db68c 100644 --- a/pkg/cli/imports.go +++ b/pkg/cli/imports.go @@ -218,8 +218,9 @@ func processIncludesWithWorkflowSpec(content string, workflow *WorkflowSpec, com for nestedScanner.Scan() { line := nestedScanner.Text() - if matches := parser.IncludeDirectivePattern.FindStringSubmatch(line); matches != nil { - includePath := strings.TrimSpace(matches[2]) + directive := parser.ParseImportDirective(line) + if directive != nil { + includePath := directive.Path // Handle section references var nestedFilePath string @@ -268,10 +269,11 @@ func processIncludesInContent(content string, workflow *WorkflowSpec, commitSHA for scanner.Scan() { line := scanner.Text() - // Check if this line is an @include or @import directive - if matches := parser.IncludeDirectivePattern.FindStringSubmatch(line); matches != nil { - isOptional := matches[1] == "?" - includePath := strings.TrimSpace(matches[2]) + // Parse import directive + directive := parser.ParseImportDirective(line) + if directive != nil { + isOptional := directive.IsOptional + includePath := directive.Path // Skip if it's already a workflowspec (contains repo/path format) if isWorkflowSpecFormat(includePath) { diff --git a/pkg/cli/remove_command.go b/pkg/cli/remove_command.go index 5daef56d0b0..3f88ded8493 100644 --- a/pkg/cli/remove_command.go +++ b/pkg/cli/remove_command.go @@ -367,7 +367,7 @@ func cleanupAllIncludes(verbose bool) error { return err } -// findIncludesInContent finds all @include and @import references in content +// findIncludesInContent finds all import references in content func findIncludesInContent(content, baseDir string, verbose bool) ([]string, error) { _ = baseDir // unused parameter for now, keeping for potential future use _ = verbose // unused parameter for now, keeping for potential future use @@ -376,8 +376,9 @@ func findIncludesInContent(content, baseDir string, verbose bool) ([]string, err scanner := bufio.NewScanner(strings.NewReader(content)) for scanner.Scan() { line := scanner.Text() - if matches := parser.IncludeDirectivePattern.FindStringSubmatch(line); matches != nil { - includePath := strings.TrimSpace(matches[2]) + directive := parser.ParseImportDirective(line) + if directive != nil { + includePath := directive.Path // Handle section references (file.md#Section) var filePath string diff --git a/pkg/parser/frontmatter.go b/pkg/parser/frontmatter.go index c9b16396b61..26cbd1472f2 100644 --- a/pkg/parser/frontmatter.go +++ b/pkg/parser/frontmatter.go @@ -15,8 +15,55 @@ import ( "github.com/goccy/go-yaml" ) -// IncludeDirectivePattern matches @include or @import directives -var IncludeDirectivePattern = regexp.MustCompile(`^@(?:include|import)(\?)?\s+(.+)$`) +// IncludeDirectivePattern matches @include, @import (deprecated), or {{#import: (new) directives +var IncludeDirectivePattern = regexp.MustCompile(`^(?:@(?:include|import)(\?)?\s+(.+)|{{#import(\?)?:\s*(.+?)\s*}})$`) + +// LegacyIncludeDirectivePattern matches only the deprecated @include and @import directives +var LegacyIncludeDirectivePattern = regexp.MustCompile(`^@(?:include|import)(\?)?\s+(.+)$`) + +// ImportDirectiveMatch holds the parsed components of an import directive +type ImportDirectiveMatch struct { + IsOptional bool + Path string + IsLegacy bool + Original string +} + +// ParseImportDirective parses an import directive and returns its components +func ParseImportDirective(line string) *ImportDirectiveMatch { + trimmedLine := strings.TrimSpace(line) + + // Check if it matches the import pattern at all + matches := IncludeDirectivePattern.FindStringSubmatch(trimmedLine) + if matches == nil { + return nil + } + + // Check if it's legacy syntax + isLegacy := LegacyIncludeDirectivePattern.MatchString(trimmedLine) + + var isOptional bool + var path string + + if isLegacy { + // Legacy syntax: @include? path or @import? path + // Group 1: optional marker, Group 2: path + isOptional = matches[1] == "?" + path = strings.TrimSpace(matches[2]) + } else { + // New syntax: {{#import?: path}} or {{#import: path}} + // Group 3: optional marker, Group 4: path + isOptional = matches[3] == "?" + path = strings.TrimSpace(matches[4]) + } + + return &ImportDirectiveMatch{ + IsOptional: isOptional, + Path: path, + IsLegacy: isLegacy, + Original: trimmedLine, + } +} // isMCPType checks if a type string represents an MCP-compatible type func isMCPType(typeStr string) bool { @@ -379,14 +426,14 @@ func ProcessImportsFromFrontmatterWithManifest(frontmatter map[string]any, baseD return toolsBuilder.String(), engines, processedFiles, nil } -// ProcessIncludes processes @include and @import directives in markdown content +// ProcessIncludes processes @include, @import (deprecated), and {{#import: directives in markdown content // This matches the bash process_includes function behavior func ProcessIncludes(content, baseDir string, extractTools bool) (string, error) { visited := make(map[string]bool) return processIncludesWithVisited(content, baseDir, extractTools, visited) } -// processIncludesWithVisited processes @include and @import directives with cycle detection +// processIncludesWithVisited processes import directives with cycle detection func processIncludesWithVisited(content, baseDir string, extractTools bool, visited map[string]bool) (string, error) { scanner := bufio.NewScanner(strings.NewReader(content)) var result bytes.Buffer @@ -394,10 +441,19 @@ func processIncludesWithVisited(content, baseDir string, extractTools bool, visi for scanner.Scan() { line := scanner.Text() - // Check if this line is an @include or @import directive - if matches := IncludeDirectivePattern.FindStringSubmatch(line); matches != nil { - isOptional := matches[1] == "?" - includePath := strings.TrimSpace(matches[2]) + // Parse import directive + directive := ParseImportDirective(line) + if directive != nil { + // Emit deprecation warning for legacy syntax + if directive.IsLegacy { + fmt.Fprintln(os.Stderr, console.FormatWarningMessage(fmt.Sprintf("Deprecated syntax: '%s'. Use '{{#import%s: %s}}' instead.", + directive.Original, + map[bool]string{true: "?", false: ""}[directive.IsOptional], + directive.Path))) + } + + isOptional := directive.IsOptional + includePath := directive.Path // Handle section references (file.md#Section) var filePath, sectionName string diff --git a/pkg/parser/import_syntax_test.go b/pkg/parser/import_syntax_test.go new file mode 100644 index 00000000000..86c0c93e8f8 --- /dev/null +++ b/pkg/parser/import_syntax_test.go @@ -0,0 +1,223 @@ +package parser + +import ( + "os" + "strings" + "testing" +) + +func TestParseImportDirective(t *testing.T) { + tests := []struct { + name string + input string + wantMatch bool + wantPath string + wantOptional bool + wantLegacy bool + }{ + // New syntax tests + { + name: "new syntax - basic import", + input: "{{#import: shared/tools.md}}", + wantMatch: true, + wantPath: "shared/tools.md", + wantOptional: false, + wantLegacy: false, + }, + { + name: "new syntax - optional import", + input: "{{#import?: shared/tools.md}}", + wantMatch: true, + wantPath: "shared/tools.md", + wantOptional: true, + wantLegacy: false, + }, + { + name: "new syntax - with extra spaces", + input: "{{#import: shared/tools.md }}", + wantMatch: true, + wantPath: "shared/tools.md", + wantOptional: false, + wantLegacy: false, + }, + { + name: "new syntax - with section", + input: "{{#import: shared/tools.md#Security}}", + wantMatch: true, + wantPath: "shared/tools.md#Security", + wantOptional: false, + wantLegacy: false, + }, + { + name: "new syntax - optional with section", + input: "{{#import?: shared/tools.md#Security}}", + wantMatch: true, + wantPath: "shared/tools.md#Security", + wantOptional: true, + wantLegacy: false, + }, + // Legacy syntax tests + { + name: "legacy - @include basic", + input: "@include shared/tools.md", + wantMatch: true, + wantPath: "shared/tools.md", + wantOptional: false, + wantLegacy: true, + }, + { + name: "legacy - @include optional", + input: "@include? shared/tools.md", + wantMatch: true, + wantPath: "shared/tools.md", + wantOptional: true, + wantLegacy: true, + }, + { + name: "legacy - @import basic", + input: "@import shared/config.md", + wantMatch: true, + wantPath: "shared/config.md", + wantOptional: false, + wantLegacy: true, + }, + { + name: "legacy - @import optional", + input: "@import? shared/config.md", + wantMatch: true, + wantPath: "shared/config.md", + wantOptional: true, + wantLegacy: true, + }, + { + name: "legacy - with section", + input: "@include shared/tools.md#Section", + wantMatch: true, + wantPath: "shared/tools.md#Section", + wantOptional: false, + wantLegacy: true, + }, + // Non-matching tests + { + name: "no match - regular text", + input: "This is regular text", + wantMatch: false, + }, + { + name: "no match - incomplete new syntax", + input: "{{#import shared/tools.md}}", + wantMatch: false, + }, + { + name: "no match - missing colon in new syntax", + input: "{{#import shared/tools.md}}", + wantMatch: false, + }, + { + name: "no match - legacy without path", + input: "@include", + wantMatch: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := ParseImportDirective(tt.input) + + if tt.wantMatch { + if result == nil { + t.Errorf("ParseImportDirective() returned nil, want match") + return + } + + if result.Path != tt.wantPath { + t.Errorf("ParseImportDirective() Path = %q, want %q", result.Path, tt.wantPath) + } + + if result.IsOptional != tt.wantOptional { + t.Errorf("ParseImportDirective() IsOptional = %v, want %v", result.IsOptional, tt.wantOptional) + } + + if result.IsLegacy != tt.wantLegacy { + t.Errorf("ParseImportDirective() IsLegacy = %v, want %v", result.IsLegacy, tt.wantLegacy) + } + + if result.Original != strings.TrimSpace(tt.input) { + t.Errorf("ParseImportDirective() Original = %q, want %q", result.Original, strings.TrimSpace(tt.input)) + } + } else { + if result != nil { + t.Errorf("ParseImportDirective() returned %+v, want nil", result) + } + } + }) + } +} + +func TestProcessIncludesWithNewSyntax(t *testing.T) { + // Create temporary test files + tempDir := t.TempDir() + + // Create test file with markdown content + testFile := tempDir + "/test.md" + testContent := `--- +tools: + bash: + allowed: ["ls", "cat"] +--- + +# Test Content +This is a test file content. +` + if err := os.WriteFile(testFile, []byte(testContent), 0644); err != nil { + t.Fatalf("Failed to write test file: %v", err) + } + + tests := []struct { + name string + content string + expected string + wantErr bool + }{ + { + name: "new syntax - basic import", + content: "{{#import: test.md}}\n# After import", + expected: "# Test Content\nThis is a test file content.\n# After import\n", + wantErr: false, + }, + { + name: "new syntax - optional import (file exists)", + content: "{{#import?: test.md}}\n# After import", + expected: "# Test Content\nThis is a test file content.\n# After import\n", + wantErr: false, + }, + { + name: "new syntax - optional import (file missing)", + content: "{{#import?: nonexistent.md}}\n# After import", + expected: "# After import\n", + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := ProcessIncludes(tt.content, tempDir, false) + + if tt.wantErr { + if err == nil { + t.Errorf("ProcessIncludes() expected error, got nil") + } + return + } + + if err != nil { + t.Errorf("ProcessIncludes() unexpected error = %v", err) + return + } + + if result != tt.expected { + t.Errorf("ProcessIncludes() result = %q, want %q", result, tt.expected) + } + }) + } +} diff --git a/pkg/workflow/compiler_template_validation_test.go b/pkg/workflow/compiler_template_validation_test.go index 3c48eb1a4e7..e06565a254c 100644 --- a/pkg/workflow/compiler_template_validation_test.go +++ b/pkg/workflow/compiler_template_validation_test.go @@ -65,7 +65,7 @@ permissions: @import shared/config.md {{/if}}`, shouldError: true, - errContains: "@include/@import directives cannot be used inside template regions", + errContains: "import directives cannot be used inside template regions", }, { name: "valid workflow with multiple templates and includes between them", diff --git a/pkg/workflow/template.go b/pkg/workflow/template.go index 28b3b2e4bbd..a19a97b9a6d 100644 --- a/pkg/workflow/template.go +++ b/pkg/workflow/template.go @@ -38,7 +38,7 @@ func wrapExpressionsInTemplateConditionals(markdown string) string { return result } -// validateNoIncludesInTemplateRegions checks that @include/@import directives +// validateNoIncludesInTemplateRegions checks that import directives // are not used inside template conditional blocks ({{#if...}}{{/if}}) func validateNoIncludesInTemplateRegions(markdown string) error { // Find all template regions by matching {{#if...}}...{{/if}} blocks @@ -55,21 +55,14 @@ func validateNoIncludesInTemplateRegions(markdown string) error { // Check the content inside the template region (capture group 1) regionContent := match[1] - // Check for @include or @import directives in this region + // Check for import directives in this region lines := strings.Split(regionContent, "\n") for lineNum, line := range lines { // Trim leading/trailing whitespace before checking trimmedLine := strings.TrimSpace(line) - if parser.IncludeDirectivePattern.MatchString(trimmedLine) { - // Found an include directive inside a template region - // Extract just the directive for error message - matches := parser.IncludeDirectivePattern.FindStringSubmatch(trimmedLine) - directive := trimmedLine - if len(matches) > 0 { - directive = matches[0] - } - - return fmt.Errorf("@include/@import directives cannot be used inside template regions ({{#if...}}{{/if}}): found '%s' at line %d within template block", directive, lineNum+1) + directive := parser.ParseImportDirective(trimmedLine) + if directive != nil { + return fmt.Errorf("import directives cannot be used inside template regions ({{#if...}}{{/if}}): found '%s' at line %d within template block", directive.Original, lineNum+1) } } } diff --git a/pkg/workflow/template_include_validation_test.go b/pkg/workflow/template_include_validation_test.go index f518faab7dc..b4acaefc31c 100644 --- a/pkg/workflow/template_include_validation_test.go +++ b/pkg/workflow/template_include_validation_test.go @@ -32,7 +32,7 @@ This is inside a template. Some content here. {{/if}}`, wantErr: true, - errMsg: "@include/@import directives cannot be used inside template regions", + errMsg: "import directives cannot be used inside template regions", }, { name: "invalid - import inside template region", @@ -42,7 +42,7 @@ Some content here. @import shared/config.md {{/if}}`, wantErr: true, - errMsg: "@include/@import directives cannot be used inside template regions", + errMsg: "import directives cannot be used inside template regions", }, { name: "invalid - optional include inside template region", @@ -52,7 +52,7 @@ Some content here. @include? shared/optional.md {{/if}}`, wantErr: true, - errMsg: "@include/@import directives cannot be used inside template regions", + errMsg: "import directives cannot be used inside template regions", }, { name: "valid - multiple includes outside templates", @@ -102,7 +102,7 @@ First template - no include. Second template - has include. {{/if}}`, wantErr: true, - errMsg: "@include/@import directives cannot be used inside template regions", + errMsg: "import directives cannot be used inside template regions", }, { name: "valid - nested content but include outside", @@ -125,7 +125,7 @@ Some content. @include shared/tools.md#Security {{/if}}`, wantErr: true, - errMsg: "@include/@import directives cannot be used inside template regions", + errMsg: "import directives cannot be used inside template regions", }, { name: "valid - include with section reference outside template", @@ -151,7 +151,7 @@ with multiple lines of content. More content after the include. {{/if}}`, wantErr: true, - errMsg: "@include/@import directives cannot be used inside template regions", + errMsg: "import directives cannot be used inside template regions", }, { name: "valid - template inside template outside (complex nesting)", @@ -189,7 +189,7 @@ Content 2 @include shared/tools.md {{/if}}`, wantErr: true, - errMsg: "@include/@import directives cannot be used inside template regions", + errMsg: "import directives cannot be used inside template regions", }, { name: "valid - no templates or includes", @@ -207,7 +207,7 @@ No templates or includes here.`, @include shared/tools.md {{/if}}`, wantErr: true, - errMsg: "@include/@import directives cannot be used inside template regions", + errMsg: "import directives cannot be used inside template regions", }, { name: "invalid - nested template with include in inner block", @@ -223,7 +223,7 @@ First level template. End of first level. {{/if}}`, wantErr: true, - errMsg: "@include/@import directives cannot be used inside template regions", + errMsg: "import directives cannot be used inside template regions", }, } From aebb35f402eabd015018d7456b777ebd1a57df1b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 6 Oct 2025 20:03:58 +0000 Subject: [PATCH 3/5] Update documentation and examples to use new import syntax Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .github/workflows/dev.md | 2 +- .../content/docs/guides/packaging-imports.md | 36 +++-- pkg/parser/frontmatter.go | 25 ++-- pkg/parser/import_syntax_test.go | 134 +++++++++--------- 4 files changed, 108 insertions(+), 89 deletions(-) diff --git a/.github/workflows/dev.md b/.github/workflows/dev.md index ae765cbfa63..b534d208374 100644 --- a/.github/workflows/dev.md +++ b/.github/workflows/dev.md @@ -20,7 +20,7 @@ safe-outputs: # Poem Generator -@include shared/use-emojis.md +{{#import: shared/use-emojis.md}} {{#if ${{ github.event.inputs.funny }}}} Be funny and creative! Make the poem humorous and entertaining. diff --git a/docs/src/content/docs/guides/packaging-imports.md b/docs/src/content/docs/guides/packaging-imports.md index 92e834941d9..2e4a4c2519a 100644 --- a/docs/src/content/docs/guides/packaging-imports.md +++ b/docs/src/content/docs/guides/packaging-imports.md @@ -269,19 +269,19 @@ Import directives allow you to modularize and reuse workflow components across m ### Basic Import Syntax ```aw wrap -@import relative/path/to/file.md +{{#import: relative/path/to/file.md}} ``` Imports files relative to the current markdown file's location. :::note -`@import` and `@include` are aliases - you can use either keyword interchangeably. +**New Syntax:** Use `{{#import: path}}` (recommended). The old `@import` and `@include` syntax is deprecated and will show warnings. ::: ### Optional Imports ```aw wrap -@import? relative/path/to/file.md +{{#import?: relative/path/to/file.md}} ``` Imports files optionally - if the file doesn't exist, no error occurs and a friendly informational comment is added to the workflow. The optional file will be watched for changes in `gh aw compile --watch` mode, so creating the file later will automatically import it. @@ -289,7 +289,7 @@ Imports files optionally - if the file doesn't exist, no error occurs and a frie ### Section-Specific Imports ```aw wrap -@import filename.md#Section +{{#import: filename.md#Section}} ``` Imports only a specific section from a markdown file using the section header. @@ -315,7 +315,7 @@ tools: allowed: [get_issue] --- -@import shared/extra-tools.md # Adds more GitHub tools +{{#import: shared/extra-tools.md}} # Adds more GitHub tools ``` ```aw wrap @@ -332,16 +332,16 @@ tools: ### Import Processing During Add -When adding a workflow with the `add` command, local file references in `@include` directives are automatically converted to workflow specifications: +When adding a workflow with the `add` command, local file references in import directives are automatically converted to workflow specifications: **Before (in source repository):** ```aw wrap -@include shared/security-notice.md +{{#import: shared/security-notice.md}} ``` **After (in your repository):** ```aw wrap -@include githubnext/agentics/shared/security-notice.md@abc123def +{{#import: githubnext/agentics/shared/security-notice.md@abc123def}} ``` This ensures that included files continue to reference the source repository, maintaining consistency and enabling updates. @@ -380,6 +380,24 @@ imports: This maintains references to the source repository and enables proper version tracking. +### Legacy Syntax (Deprecated) + +:::caution[Deprecated] +The `@include` and `@import` syntax is deprecated. Use `{{#import: path}}` instead. The old syntax will continue to work but will display deprecation warnings during compilation. + +**Migration example:** +```diff +- @include shared/tools.md ++ {{#import: shared/tools.md}} + +- @include? shared/optional.md ++ {{#import?: shared/optional.md}} + +- @import shared/config.md#Section ++ {{#import: shared/config.md#Section}} +``` +::: + ## Practical Examples ### Example 1: Adding a Versioned Workflow @@ -480,7 +498,7 @@ permissions: # Issue Analyzer -@include shared/security-notice.md +{{#import: shared/security-notice.md}} Analyze the issue and provide helpful feedback. ``` diff --git a/pkg/parser/frontmatter.go b/pkg/parser/frontmatter.go index 26cbd1472f2..6589d705f0e 100644 --- a/pkg/parser/frontmatter.go +++ b/pkg/parser/frontmatter.go @@ -32,19 +32,19 @@ type ImportDirectiveMatch struct { // ParseImportDirective parses an import directive and returns its components func ParseImportDirective(line string) *ImportDirectiveMatch { trimmedLine := strings.TrimSpace(line) - + // Check if it matches the import pattern at all matches := IncludeDirectivePattern.FindStringSubmatch(trimmedLine) if matches == nil { return nil } - + // Check if it's legacy syntax isLegacy := LegacyIncludeDirectivePattern.MatchString(trimmedLine) - + var isOptional bool var path string - + if isLegacy { // Legacy syntax: @include? path or @import? path // Group 1: optional marker, Group 2: path @@ -56,7 +56,7 @@ func ParseImportDirective(line string) *ImportDirectiveMatch { isOptional = matches[3] == "?" path = strings.TrimSpace(matches[4]) } - + return &ImportDirectiveMatch{ IsOptional: isOptional, Path: path, @@ -446,8 +446,8 @@ func processIncludesWithVisited(content, baseDir string, extractTools bool, visi if directive != nil { // Emit deprecation warning for legacy syntax if directive.IsLegacy { - fmt.Fprintln(os.Stderr, console.FormatWarningMessage(fmt.Sprintf("Deprecated syntax: '%s'. Use '{{#import%s: %s}}' instead.", - directive.Original, + fmt.Fprintln(os.Stderr, console.FormatWarningMessage(fmt.Sprintf("Deprecated syntax: '%s'. Use '{{#import%s: %s}}' instead.", + directive.Original, map[bool]string{true: "?", false: ""}[directive.IsOptional], directive.Path))) } @@ -884,7 +884,7 @@ func ExpandIncludesForEngines(content, baseDir string) ([]string, error) { return engines, nil } -// ProcessIncludesForEngines processes @include and @import directives to extract engine configurations +// ProcessIncludesForEngines processes import directives to extract engine configurations func ProcessIncludesForEngines(content, baseDir string) ([]string, string, error) { scanner := bufio.NewScanner(strings.NewReader(content)) var result bytes.Buffer @@ -893,10 +893,11 @@ func ProcessIncludesForEngines(content, baseDir string) ([]string, string, error for scanner.Scan() { line := scanner.Text() - // Check if this line is an @include or @import directive - if matches := IncludeDirectivePattern.FindStringSubmatch(line); matches != nil { - isOptional := matches[1] == "?" - includePath := strings.TrimSpace(matches[2]) + // Parse import directive + directive := ParseImportDirective(line) + if directive != nil { + isOptional := directive.IsOptional + includePath := directive.Path // Handle section references (file.md#Section) - for engines, we ignore sections var filePath string diff --git a/pkg/parser/import_syntax_test.go b/pkg/parser/import_syntax_test.go index 86c0c93e8f8..7050867b23f 100644 --- a/pkg/parser/import_syntax_test.go +++ b/pkg/parser/import_syntax_test.go @@ -8,115 +8,115 @@ import ( func TestParseImportDirective(t *testing.T) { tests := []struct { - name string - input string - wantMatch bool - wantPath string + name string + input string + wantMatch bool + wantPath string wantOptional bool - wantLegacy bool + wantLegacy bool }{ // New syntax tests { - name: "new syntax - basic import", - input: "{{#import: shared/tools.md}}", - wantMatch: true, - wantPath: "shared/tools.md", + name: "new syntax - basic import", + input: "{{#import: shared/tools.md}}", + wantMatch: true, + wantPath: "shared/tools.md", wantOptional: false, - wantLegacy: false, + wantLegacy: false, }, { - name: "new syntax - optional import", - input: "{{#import?: shared/tools.md}}", - wantMatch: true, - wantPath: "shared/tools.md", + name: "new syntax - optional import", + input: "{{#import?: shared/tools.md}}", + wantMatch: true, + wantPath: "shared/tools.md", wantOptional: true, - wantLegacy: false, + wantLegacy: false, }, { - name: "new syntax - with extra spaces", - input: "{{#import: shared/tools.md }}", - wantMatch: true, - wantPath: "shared/tools.md", + name: "new syntax - with extra spaces", + input: "{{#import: shared/tools.md }}", + wantMatch: true, + wantPath: "shared/tools.md", wantOptional: false, - wantLegacy: false, + wantLegacy: false, }, { - name: "new syntax - with section", - input: "{{#import: shared/tools.md#Security}}", - wantMatch: true, - wantPath: "shared/tools.md#Security", + name: "new syntax - with section", + input: "{{#import: shared/tools.md#Security}}", + wantMatch: true, + wantPath: "shared/tools.md#Security", wantOptional: false, - wantLegacy: false, + wantLegacy: false, }, { - name: "new syntax - optional with section", - input: "{{#import?: shared/tools.md#Security}}", - wantMatch: true, - wantPath: "shared/tools.md#Security", + name: "new syntax - optional with section", + input: "{{#import?: shared/tools.md#Security}}", + wantMatch: true, + wantPath: "shared/tools.md#Security", wantOptional: true, - wantLegacy: false, + wantLegacy: false, }, // Legacy syntax tests { - name: "legacy - @include basic", - input: "@include shared/tools.md", - wantMatch: true, - wantPath: "shared/tools.md", + name: "legacy - @include basic", + input: "@include shared/tools.md", + wantMatch: true, + wantPath: "shared/tools.md", wantOptional: false, - wantLegacy: true, + wantLegacy: true, }, { - name: "legacy - @include optional", - input: "@include? shared/tools.md", - wantMatch: true, - wantPath: "shared/tools.md", + name: "legacy - @include optional", + input: "@include? shared/tools.md", + wantMatch: true, + wantPath: "shared/tools.md", wantOptional: true, - wantLegacy: true, + wantLegacy: true, }, { - name: "legacy - @import basic", - input: "@import shared/config.md", - wantMatch: true, - wantPath: "shared/config.md", + name: "legacy - @import basic", + input: "@import shared/config.md", + wantMatch: true, + wantPath: "shared/config.md", wantOptional: false, - wantLegacy: true, + wantLegacy: true, }, { - name: "legacy - @import optional", - input: "@import? shared/config.md", - wantMatch: true, - wantPath: "shared/config.md", + name: "legacy - @import optional", + input: "@import? shared/config.md", + wantMatch: true, + wantPath: "shared/config.md", wantOptional: true, - wantLegacy: true, + wantLegacy: true, }, { - name: "legacy - with section", - input: "@include shared/tools.md#Section", - wantMatch: true, - wantPath: "shared/tools.md#Section", + name: "legacy - with section", + input: "@include shared/tools.md#Section", + wantMatch: true, + wantPath: "shared/tools.md#Section", wantOptional: false, - wantLegacy: true, + wantLegacy: true, }, // Non-matching tests { - name: "no match - regular text", - input: "This is regular text", - wantMatch: false, + name: "no match - regular text", + input: "This is regular text", + wantMatch: false, }, { - name: "no match - incomplete new syntax", - input: "{{#import shared/tools.md}}", - wantMatch: false, + name: "no match - incomplete new syntax", + input: "{{#import shared/tools.md}}", + wantMatch: false, }, { - name: "no match - missing colon in new syntax", - input: "{{#import shared/tools.md}}", - wantMatch: false, + name: "no match - missing colon in new syntax", + input: "{{#import shared/tools.md}}", + wantMatch: false, }, { - name: "no match - legacy without path", - input: "@include", - wantMatch: false, + name: "no match - legacy without path", + input: "@include", + wantMatch: false, }, } From d895f4678b77416248d1cb1aa90502396720ed2e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 6 Oct 2025 20:23:21 +0000 Subject: [PATCH 4/5] Make colon optional in import syntax: {{#import path}} or {{#import: path}} Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .github/workflows/dev.md | 2 +- .../content/docs/guides/packaging-imports.md | 35 +++++++++++-- pkg/parser/frontmatter.go | 9 ++-- pkg/parser/import_syntax_test.go | 51 ++++++++++++++----- 4 files changed, 75 insertions(+), 22 deletions(-) diff --git a/.github/workflows/dev.md b/.github/workflows/dev.md index b534d208374..c891e56a006 100644 --- a/.github/workflows/dev.md +++ b/.github/workflows/dev.md @@ -20,7 +20,7 @@ safe-outputs: # Poem Generator -{{#import: shared/use-emojis.md}} +{{#import shared/use-emojis.md}} {{#if ${{ github.event.inputs.funny }}}} Be funny and creative! Make the poem humorous and entertaining. diff --git a/docs/src/content/docs/guides/packaging-imports.md b/docs/src/content/docs/guides/packaging-imports.md index 2e4a4c2519a..d3ac027e91c 100644 --- a/docs/src/content/docs/guides/packaging-imports.md +++ b/docs/src/content/docs/guides/packaging-imports.md @@ -268,6 +268,12 @@ Import directives allow you to modularize and reuse workflow components across m ### Basic Import Syntax +```aw wrap +{{#import relative/path/to/file.md}} +``` + +Or with an optional colon: + ```aw wrap {{#import: relative/path/to/file.md}} ``` @@ -275,11 +281,17 @@ Import directives allow you to modularize and reuse workflow components across m Imports files relative to the current markdown file's location. :::note -**New Syntax:** Use `{{#import: path}}` (recommended). The old `@import` and `@include` syntax is deprecated and will show warnings. +**New Syntax:** Use `{{#import path}}` or `{{#import: path}}` (recommended). The colon is optional and ignored if present. The old `@import` and `@include` syntax is deprecated and will show warnings. ::: ### Optional Imports +```aw wrap +{{#import? relative/path/to/file.md}} +``` + +Or with a colon: + ```aw wrap {{#import?: relative/path/to/file.md}} ``` @@ -288,6 +300,12 @@ Imports files optionally - if the file doesn't exist, no error occurs and a frie ### Section-Specific Imports +```aw wrap +{{#import filename.md#Section}} +``` + +Or with a colon: + ```aw wrap {{#import: filename.md#Section}} ``` @@ -383,18 +401,25 @@ This maintains references to the source repository and enables proper version tr ### Legacy Syntax (Deprecated) :::caution[Deprecated] -The `@include` and `@import` syntax is deprecated. Use `{{#import: path}}` instead. The old syntax will continue to work but will display deprecation warnings during compilation. +The `@include` and `@import` syntax is deprecated. Use `{{#import path}}` or `{{#import: path}}` instead (the colon is optional). The old syntax will continue to work but will display deprecation warnings during compilation. **Migration example:** ```diff - @include shared/tools.md -+ {{#import: shared/tools.md}} ++ {{#import shared/tools.md}} - @include? shared/optional.md -+ {{#import?: shared/optional.md}} ++ {{#import? shared/optional.md}} - @import shared/config.md#Section -+ {{#import: shared/config.md#Section}} ++ {{#import shared/config.md#Section}} +``` + +You can also use the colon if preferred: +```aw wrap +{{#import: shared/tools.md}} +{{#import?: shared/optional.md}} +{{#import: shared/config.md#Section}} ``` ::: diff --git a/pkg/parser/frontmatter.go b/pkg/parser/frontmatter.go index 6589d705f0e..89502d06a53 100644 --- a/pkg/parser/frontmatter.go +++ b/pkg/parser/frontmatter.go @@ -15,8 +15,9 @@ import ( "github.com/goccy/go-yaml" ) -// IncludeDirectivePattern matches @include, @import (deprecated), or {{#import: (new) directives -var IncludeDirectivePattern = regexp.MustCompile(`^(?:@(?:include|import)(\?)?\s+(.+)|{{#import(\?)?:\s*(.+?)\s*}})$`) +// IncludeDirectivePattern matches @include, @import (deprecated), or {{#import (new) directives +// The colon after #import is optional and ignored if present +var IncludeDirectivePattern = regexp.MustCompile(`^(?:@(?:include|import)(\?)?\s+(.+)|{{#import(\?)?\s*:?\s*(.+?)\s*}})$`) // LegacyIncludeDirectivePattern matches only the deprecated @include and @import directives var LegacyIncludeDirectivePattern = regexp.MustCompile(`^@(?:include|import)(\?)?\s+(.+)$`) @@ -51,7 +52,7 @@ func ParseImportDirective(line string) *ImportDirectiveMatch { isOptional = matches[1] == "?" path = strings.TrimSpace(matches[2]) } else { - // New syntax: {{#import?: path}} or {{#import: path}} + // New syntax: {{#import?: path}} or {{#import: path}} (colon is optional) // Group 3: optional marker, Group 4: path isOptional = matches[3] == "?" path = strings.TrimSpace(matches[4]) @@ -446,7 +447,7 @@ func processIncludesWithVisited(content, baseDir string, extractTools bool, visi if directive != nil { // Emit deprecation warning for legacy syntax if directive.IsLegacy { - fmt.Fprintln(os.Stderr, console.FormatWarningMessage(fmt.Sprintf("Deprecated syntax: '%s'. Use '{{#import%s: %s}}' instead.", + fmt.Fprintln(os.Stderr, console.FormatWarningMessage(fmt.Sprintf("Deprecated syntax: '%s'. Use '{{#import%s %s}}' instead.", directive.Original, map[bool]string{true: "?", false: ""}[directive.IsOptional], directive.Path))) diff --git a/pkg/parser/import_syntax_test.go b/pkg/parser/import_syntax_test.go index 7050867b23f..2459324d51c 100644 --- a/pkg/parser/import_syntax_test.go +++ b/pkg/parser/import_syntax_test.go @@ -56,6 +56,31 @@ func TestParseImportDirective(t *testing.T) { wantOptional: true, wantLegacy: false, }, + // New syntax without colon tests + { + name: "new syntax - basic import without colon", + input: "{{#import shared/tools.md}}", + wantMatch: true, + wantPath: "shared/tools.md", + wantOptional: false, + wantLegacy: false, + }, + { + name: "new syntax - optional import without colon", + input: "{{#import? shared/tools.md}}", + wantMatch: true, + wantPath: "shared/tools.md", + wantOptional: true, + wantLegacy: false, + }, + { + name: "new syntax - with section without colon", + input: "{{#import shared/tools.md#Security}}", + wantMatch: true, + wantPath: "shared/tools.md#Security", + wantOptional: false, + wantLegacy: false, + }, // Legacy syntax tests { name: "legacy - @include basic", @@ -103,16 +128,6 @@ func TestParseImportDirective(t *testing.T) { input: "This is regular text", wantMatch: false, }, - { - name: "no match - incomplete new syntax", - input: "{{#import shared/tools.md}}", - wantMatch: false, - }, - { - name: "no match - missing colon in new syntax", - input: "{{#import shared/tools.md}}", - wantMatch: false, - }, { name: "no match - legacy without path", input: "@include", @@ -180,17 +195,29 @@ This is a test file content. wantErr bool }{ { - name: "new syntax - basic import", + name: "new syntax - basic import with colon", content: "{{#import: test.md}}\n# After import", expected: "# Test Content\nThis is a test file content.\n# After import\n", wantErr: false, }, { - name: "new syntax - optional import (file exists)", + name: "new syntax - basic import without colon", + content: "{{#import test.md}}\n# After import", + expected: "# Test Content\nThis is a test file content.\n# After import\n", + wantErr: false, + }, + { + name: "new syntax - optional import with colon (file exists)", content: "{{#import?: test.md}}\n# After import", expected: "# Test Content\nThis is a test file content.\n# After import\n", wantErr: false, }, + { + name: "new syntax - optional import without colon (file exists)", + content: "{{#import? test.md}}\n# After import", + expected: "# Test Content\nThis is a test file content.\n# After import\n", + wantErr: false, + }, { name: "new syntax - optional import (file missing)", content: "{{#import?: nonexistent.md}}\n# After import", From 2bd4fa5cee21ba57b00d5908b0c8bc9b0fb48c9c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 6 Oct 2025 20:35:01 +0000 Subject: [PATCH 5/5] Remove documentation about optional colon syntax Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .../content/docs/guides/packaging-imports.md | 31 +------------------ 1 file changed, 1 insertion(+), 30 deletions(-) diff --git a/docs/src/content/docs/guides/packaging-imports.md b/docs/src/content/docs/guides/packaging-imports.md index d3ac027e91c..c0a0c13005e 100644 --- a/docs/src/content/docs/guides/packaging-imports.md +++ b/docs/src/content/docs/guides/packaging-imports.md @@ -272,30 +272,14 @@ Import directives allow you to modularize and reuse workflow components across m {{#import relative/path/to/file.md}} ``` -Or with an optional colon: - -```aw wrap -{{#import: relative/path/to/file.md}} -``` - Imports files relative to the current markdown file's location. -:::note -**New Syntax:** Use `{{#import path}}` or `{{#import: path}}` (recommended). The colon is optional and ignored if present. The old `@import` and `@include` syntax is deprecated and will show warnings. -::: - ### Optional Imports ```aw wrap {{#import? relative/path/to/file.md}} ``` -Or with a colon: - -```aw wrap -{{#import?: relative/path/to/file.md}} -``` - Imports files optionally - if the file doesn't exist, no error occurs and a friendly informational comment is added to the workflow. The optional file will be watched for changes in `gh aw compile --watch` mode, so creating the file later will automatically import it. ### Section-Specific Imports @@ -304,12 +288,6 @@ Imports files optionally - if the file doesn't exist, no error occurs and a frie {{#import filename.md#Section}} ``` -Or with a colon: - -```aw wrap -{{#import: filename.md#Section}} -``` - Imports only a specific section from a markdown file using the section header. ### Import Path Resolution @@ -401,7 +379,7 @@ This maintains references to the source repository and enables proper version tr ### Legacy Syntax (Deprecated) :::caution[Deprecated] -The `@include` and `@import` syntax is deprecated. Use `{{#import path}}` or `{{#import: path}}` instead (the colon is optional). The old syntax will continue to work but will display deprecation warnings during compilation. +The `@include` and `@import` syntax is deprecated. Use `{{#import path}}` instead. The old syntax will continue to work but will display deprecation warnings during compilation. **Migration example:** ```diff @@ -414,13 +392,6 @@ The `@include` and `@import` syntax is deprecated. Use `{{#import path}}` or `{{ - @import shared/config.md#Section + {{#import shared/config.md#Section}} ``` - -You can also use the colon if preferred: -```aw wrap -{{#import: shared/tools.md}} -{{#import?: shared/optional.md}} -{{#import: shared/config.md#Section}} -``` ::: ## Practical Examples