ParseWorkflowString parses workflow markdown content from a string rather than a file. This is the primary entry point for Wasm/browser usage where filesystem access is unavailable. The virtualPath is used for error messages and lock file naming (e.g., "workflow.md").
(content string, virtualPath string)
| 55 | // This is the primary entry point for Wasm/browser usage where filesystem access is unavailable. |
| 56 | // The virtualPath is used for error messages and lock file naming (e.g., "workflow.md"). |
| 57 | func (c *Compiler) ParseWorkflowString(content string, virtualPath string) (*WorkflowData, error) { |
| 58 | workflowLog.Printf("ParseWorkflowString: parsing %d bytes with virtual path %s", len(content), virtualPath) |
| 59 | |
| 60 | cleanPath := filepath.Clean(virtualPath) |
| 61 | contentBytes := []byte(content) |
| 62 | |
| 63 | // Store content so downstream code can use it instead of reading from disk. |
| 64 | // Cleared in CompileToYAML after compilation completes. |
| 65 | c.contentOverride = content |
| 66 | |
| 67 | // Enable inline prompt mode for string-based compilation (Wasm/browser) |
| 68 | // since runtime-import macros cannot resolve without filesystem access |
| 69 | c.inlinePrompt = true |
| 70 | |
| 71 | // Parse frontmatter directly from content string |
| 72 | result, err := parser.ExtractFrontmatterFromContent(content) |
| 73 | if err != nil { |
| 74 | frontmatterStart := 2 |
| 75 | if result != nil && result.FrontmatterStart > 0 { |
| 76 | frontmatterStart = result.FrontmatterStart |
| 77 | } |
| 78 | return nil, c.createFrontmatterError(cleanPath, content, err, frontmatterStart) |
| 79 | } |
| 80 | |
| 81 | if len(result.Frontmatter) == 0 { |
| 82 | return nil, errors.New("no frontmatter found") |
| 83 | } |
| 84 | |
| 85 | compilerStringAPILog.Printf("ParseWorkflowString: extracted frontmatter with %d fields", len(result.Frontmatter)) |
| 86 | |
| 87 | // Preprocess schedule fields |
| 88 | if err := c.preprocessScheduleFields(result.Frontmatter, cleanPath, content); err != nil { |
| 89 | return nil, err |
| 90 | } |
| 91 | |
| 92 | frontmatterForValidation := c.copyFrontmatterWithoutInternalMarkers(result.Frontmatter) |
| 93 | |
| 94 | // Check if "on" field is missing - distinguish redirect-only placeholders from shared workflows |
| 95 | _, hasOnField := frontmatterForValidation["on"] |
| 96 | if !hasOnField { |
| 97 | // Check if this is a redirect-only placeholder (has redirect field but no 'on' trigger). |
| 98 | // Redirect-only files are distinct from regular shared workflows: they are placeholders |
| 99 | // pointing to a workflow's new canonical location and should not be treated as importable components. |
| 100 | if redirectVal, hasRedirect := frontmatterForValidation["redirect"]; hasRedirect { |
| 101 | if redirectStr, ok := redirectVal.(string); ok { |
| 102 | if redirectTarget := strings.TrimSpace(redirectStr); redirectTarget != "" { |
| 103 | compilerStringAPILog.Printf("ParseWorkflowString: redirect-only workflow detected: redirect=%s", redirectTarget) |
| 104 | return nil, &RedirectOnlyWorkflowError{Path: cleanPath, Target: redirectTarget} |
| 105 | } |
| 106 | } |
| 107 | } |
| 108 | compilerStringAPILog.Printf("ParseWorkflowString: no 'on' field, treating as shared workflow: %s", cleanPath) |
| 109 | return nil, &SharedWorkflowError{Path: cleanPath} |
| 110 | } |
| 111 | |
| 112 | if err := c.validateEngineBeforeSchema(cleanPath, contentBytes, result, frontmatterForValidation); err != nil { |
| 113 | compilerStringAPILog.Printf("ParseWorkflowString: string engine pre-validation failed for %s", cleanPath) |
| 114 | return nil, err |