2026-09-01 16:37:53 +00:00
using System.Text ;
using System.Text.Json ;
using w4c_workflows.Models ;
namespace w4c_workflows.Services ;
/// <summary>
/// Renders a compiled workflow into a self-contained, non-Mermaid HTML document
/// that any client (the SPA, a standalone page, an email, a README) can embed in
/// an <c><iframe></c> — the backend owns the layout, so no client-side
/// Mermaid/CSS graph library is required.
///
/// Instead of a terse node graph it produces a *rich, detailed* read: the success
/// chain as a stepped timeline, the failure/compensation edges, and a full task
/// inventory (entry, language, mode, env, parent/next/onError references). All
/// styling is inline in the document (CSS variables for light/dark), so the
/// output is portable and themeable.
/// </summary>
public sealed class WorkflowHtmlRenderer
{
private static readonly JsonSerializerOptions Json = new ( JsonSerializerDefaults . Web ) ;
private readonly Microsoft . Extensions . Logging . ILogger < WorkflowHtmlRenderer > _logger ;
public WorkflowHtmlRenderer ( Microsoft . Extensions . Logging . ILogger < WorkflowHtmlRenderer > logger )
{
_logger = logger ;
}
public string Render ( Workflow workflow , IReadOnlyList < WorkflowTask > tasks , WorkflowDefinition ? def , bool dark )
{
var sb = new StringBuilder ( 1 < < 14 ) ;
var t = tasks . OrderBy ( x = > x . Order ) . ToList ( ) ;
var byId = t . ToDictionary ( x = > x . Id ) ;
var chain = HappyPathChain ( t , byId ) ;
var errors = t . Where ( x = > x . OnErrorId ! = null ) . ToList ( ) ;
var trigger = ParseTrigger ( workflow . TriggerJson ) ;
var theme = dark ? "dark" : "light" ;
var title = $"{workflow.Name} — workflow" ;
sb . Append ( "<!DOCTYPE html>\n<html lang=\"en\" data-theme=\"" ) . Append ( theme ) . Append ( "\">\n" ) ;
sb . Append ( "<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n" ) ;
sb . Append ( "<title>" ) . Append ( Html ( title ) ) . Append ( "</title>\n" ) ;
sb . Append ( "<style>\n" ) ;
sb . Append ( Css ) ;
sb . Append ( "\n</style>\n</head>\n<body>\n<main class=\"wf\">\n" ) ;
// ---- Header ---------------------------------------------------------
sb . Append ( "<header class=\"wf-head\">\n" ) ;
sb . Append ( "<div class=\"wf-head-row\">\n" ) ;
sb . Append ( "<h1 class=\"wf-name\">" ) . Append ( Html ( workflow . Name ) ) . Append ( "</h1>\n" ) ;
sb . Append ( "<div class=\"wf-badges\">\n" ) ;
Badge ( sb , workflow . Mode , "badge-mode" ) ;
Badge ( sb , $"v{workflow.Version}" , "badge-version" ) ;
Badge ( sb , $"target: {workflow.Target}" , "badge-target" ) ;
Badge ( sb , workflow . Status , workflow . Status = = WorkflowStatus . Compiled ? "badge-ok" : "badge-warn" ) ;
if ( workflow . TriggerEnabled & & trigger ! = null )
Badge ( sb , $"trigger: {trigger.Type}" , "badge-info" ) ;
sb . Append ( "</div>\n" ) ;
sb . Append ( "</div>\n" ) ;
if ( def ! = null & & ! string . IsNullOrWhiteSpace ( def . Description ) )
sb . Append ( "<p class=\"wf-desc\">" ) . Append ( Html ( def . Description ) ) . Append ( "</p>\n" ) ;
sb . Append ( "<dl class=\"wf-meta\">\n" ) ;
MetaItem ( sb , "Language" , def ? . Language ? ? FirstLanguage ( t ) ? ? "—" ) ;
MetaItem ( sb , "Entry" , def ? . Entry is { } e ? $"{e.File} :: {e.Function ?? " main "}()" : ( EntrySummary ( t ) ? ? "—" ) ) ;
MetaItem ( sb , "Tasks" , t . Count . ToString ( ) ) ;
MetaItem ( sb , "Success chain" , chain . Count . ToString ( ) ) ;
MetaItem ( sb , "Compensation" , errors . Count . ToString ( ) ) ;
if ( workflow . UpdatedAt is { } upd )
MetaItem ( sb , "Updated" , upd . ToUniversalTime ( ) . ToString ( "yyyy-MM-dd HH:mm 'UTC'" ) ) ;
sb . Append ( "</dl>\n" ) ;
if ( trigger ! = null )
{
sb . Append ( "<div class=\"wf-trigger\">\n<div class=\"wf-trigger-title\">Auto-trigger</div>\n" ) ;
sb . Append ( "<code>" ) . Append ( Html ( JsonSerializer . Serialize ( trigger , Json ) ) ) . Append ( "</code>\n</div>\n" ) ;
}
sb . Append ( "</header>\n" ) ;
// ---- Success chain --------------------------------------------------
sb . Append ( "<section class=\"wf-sec\">\n" ) ;
sb . Append ( "<h2 class=\"wf-sec-title\">Success flow</h2>\n" ) ;
if ( chain . Count = = 0 )
{
sb . Append ( "<p class=\"wf-empty\">No tasks defined.</p>\n" ) ;
}
else
{
sb . Append ( "<div class=\"wf-flow\">\n" ) ;
for ( var i = 0 ; i < chain . Count ; i + + )
{
var task = chain [ i ] ;
var last = i = = chain . Count - 1 ;
sb . Append ( "<div class=\"wf-step\">\n" ) ;
sb . Append ( "<div class=\"wf-rail\"><span class=\"wf-dot\"></span>" ) . Append ( last ? "" : "<span class=\"wf-line\"></span>" ) . Append ( "</div>\n" ) ;
sb . Append ( "<div class=\"wf-card\">\n" ) ;
sb . Append ( "<div class=\"wf-card-head\">\n" ) ;
sb . Append ( "<span class=\"wf-step-num\">" ) . Append ( i + 1 ) . Append ( "</span>\n" ) ;
sb . Append ( "<span class=\"wf-key\">" ) . Append ( Html ( task . Key ) ) . Append ( "</span>\n" ) ;
Raw ( sb , Chip ( task . Mode , "chip-mode" ) ) ;
Raw ( sb , Chip ( task . Language , $"chip-lang lang-{LangClass(task.Language)}" ) ) ;
sb . Append ( "</div>\n" ) ;
CardDetail ( sb , task , byId ) ;
sb . Append ( "</div>\n" ) ;
sb . Append ( "</div>\n" ) ;
}
sb . Append ( "</div>\n" ) ;
}
sb . Append ( "</section>\n" ) ;
// ---- Error handling ------------------------------------------------
sb . Append ( "<section class=\"wf-sec\">\n" ) ;
sb . Append ( "<h2 class=\"wf-sec-title\">Error handling & compensation</h2>\n" ) ;
if ( errors . Count = = 0 )
{
sb . Append ( "<p class=\"wf-empty\">No <code>onError</code> targets — the chain fails hard.</p>\n" ) ;
}
else
{
sb . Append ( "<div class=\"wf-errors\">\n" ) ;
foreach ( var task in errors . OrderBy ( x = > x . Order ) )
{
if ( task . OnErrorId = = null | | ! byId . TryGetValue ( task . OnErrorId . Value , out var target ) ) continue ;
sb . Append ( "<div class=\"wf-err-card\">\n" ) ;
sb . Append ( "<code class=\"wf-err-src\">" ) . Append ( Html ( task . Key ) ) . Append ( "</code>\n" ) ;
sb . Append ( "<span class=\"wf-err-arrow\">⟶ onError ⟶</span>\n" ) ;
sb . Append ( "<code class=\"wf-err-dst\">" ) . Append ( Html ( target . Key ) ) . Append ( "</code>\n" ) ;
sb . Append ( "<span class=\"wf-err-lang\">" ) . Append ( Html ( target . Language ) ) . Append ( "</span>\n" ) ;
sb . Append ( "</div>\n" ) ;
}
sb . Append ( "</div>\n" ) ;
}
sb . Append ( "</section>\n" ) ;
// ---- Task inventory -------------------------------------------------
sb . Append ( "<section class=\"wf-sec\">\n" ) ;
sb . Append ( "<h2 class=\"wf-sec-title\">Task inventory</h2>\n" ) ;
sb . Append ( "<div class=\"wf-table-wrapper\">\n" ) ;
sb . Append ( "<table class=\"wf-table\">\n" ) ;
sb . Append ( "<thead><tr><th>#</th><th>Key</th><th>Mode</th><th>Language</th><th>Entry</th><th>Function</th><th>Parent</th><th>Next</th><th>On error</th><th>Env</th></tr></thead>\n" ) ;
sb . Append ( "<tbody>\n" ) ;
for ( var i = 0 ; i < t . Count ; i + + )
{
var task = t [ i ] ;
var entry = EntryOf ( task ) ;
sb . Append ( "<tr>" ) ;
sb . Append ( "<td>" ) . Append ( i ) . Append ( "</td>" ) ;
sb . Append ( "<td><code>" ) . Append ( Html ( task . Key ) ) . Append ( "</code></td>" ) ;
sb . Append ( "<td>" ) . Append ( Html ( task . Mode ) ) . Append ( "</td>" ) ;
sb . Append ( "<td><span class=\"chip-lang lang-" ) . Append ( LangClass ( task . Language ) ) . Append ( "\">" ) . Append ( Html ( task . Language ) ) . Append ( "</span></td>" ) ;
sb . Append ( "<td>" ) . Append ( Html ( entry . File ? ? "—" ) ) . Append ( "</td>" ) ;
sb . Append ( "<td>" ) . Append ( Html ( string . IsNullOrWhiteSpace ( entry . Function ) ? "main" : entry . Function ) ) . Append ( "()</td>" ) ;
sb . Append ( "<td>" ) . Append ( Html ( KeyById ( task . ParentId , byId ) ? ? "—" ) ) . Append ( "</td>" ) ;
sb . Append ( "<td>" ) . Append ( Html ( KeyById ( task . NextId , byId ) ? ? "—" ) ) . Append ( "</td>" ) ;
sb . Append ( "<td>" ) . Append ( Html ( KeyById ( task . OnErrorId , byId ) ? ? "—" ) ) . Append ( "</td>" ) ;
sb . Append ( "<td>" ) . Append ( EnvSummary ( task ) ) . Append ( "</td>" ) ;
sb . Append ( "</tr>\n" ) ;
}
sb . Append ( "</tbody>\n</table>\n</div>\n" ) ;
sb . Append ( "</section>\n" ) ;
sb . Append ( "<footer class=\"wf-foot\">Rendered by the w4c-workflows engine at " )
. Append ( DateTime . UtcNow . ToString ( "yyyy-MM-dd HH:mm 'UTC'" ) )
. Append ( " · path <code>" ) . Append ( Html ( workflow . Path ) ) . Append ( "</code></footer>\n" ) ;
sb . Append ( "</main>\n" ) ;
sb . Append ( "<script>\n(function(){\n var cards=document.querySelectorAll('.wf-card');\n for (var i=0;i<cards.length;i++){cards[i].addEventListener('click',function(){this.classList.toggle('wf-open');});}\n})();\n</script>\n" ) ;
sb . Append ( "</body>\n</html>\n" ) ;
return sb . ToString ( ) ;
}
// ------------------------------------------------------------------ CSS
private const string Css = @ "
: root { - - bg : # f4f5f7 ; - - panel : # ffffff ; - - panel2 : # eef1f5 ; - - border : # d8dce3 ; - - text : # 1 c2330 ; - - muted : # 5f 6 b7a ; - - accent : # 3 b6fe0 ; - - ok : # 2e7d 32 ; - - warn : # b26a00 ; - - info : # 0f 6f 8f ; - - error : # c0392b ; - - root : # fde68a ; - - code : # 1e2 a3a ; }
[data-theme='dark'] { - - bg : # 121417 ; - - panel : # 1 b1f26 ; - - panel2 : # 222834 ; - - border : # 2 c333f ; - - text : # e7eaef ; - - muted : # 97 a1af ; - - accent : # 5 b8cff ; - - ok : # 4 caf6d ; - - warn : # e0a24c ; - - info : # 4f b3d9 ; - - error : # e56a5a ; - - root : # 6 a5a2a ; - - code : # c3d2e6 ; }
* { box - sizing : border - box }
body { margin : 0 ; background : var ( - - bg ) ; color : var ( - - text ) ; font : 14 px / 1.5 - apple - system , BlinkMacSystemFont , ' Segoe UI ' , Roboto , Helvetica , Arial , sans - serif ; - webkit - font - smoothing : antialiased }
main . wf { max - width : 1180 px ; margin : 0 auto ; padding : 20 px }
h1 , h2 { margin : 0 ; font - weight : 700 }
code { font - family : ui - monospace , SFMono - Regular , ' SF Mono ' , Menlo , Consolas , monospace ; font - size : 12.5 px }
. wf - head { background : var ( - - panel ) ; border : 1 px solid var ( - - border ) ; border - radius : 12 px ; padding : 20 px 22 px ; box - shadow : 0 1 px 2 px rgba ( 0 , 0 , 0 , . 04 ) }
. wf - head - row { display : flex ; flex - wrap : wrap ; gap : 12 px ; align - items : center ; justify - content : space - between }
. wf - name { font - size : 24 px ; letter - spacing : - . 01 em }
. wf - desc { margin : 10 px 0 0 ; color : var ( - - muted ) }
. wf - badges { display : flex ; flex - wrap : wrap ; gap : 6 px }
. badge { display : inline - flex ; align - items : center ; padding : 3 px 10 px ; border - radius : 999 px ; font - size : 12 px ; font - weight : 600 ; border : 1 px solid transparent }
. badge - mode { background : var ( - - accent ) ; color : # fff }
. badge - version { background : var ( - - panel2 ) ; border - color : var ( - - border ) ; color : var ( - - text ) }
. badge - target { background : var ( - - panel2 ) ; border - color : var ( - - border ) ; color : var ( - - muted ) }
. badge - ok { background : rgba ( 76 , 175 , 109 , . 16 ) ; color : var ( - - ok ) }
. badge - warn { background : rgba ( 224 , 162 , 76 , . 18 ) ; color : var ( - - warn ) }
. badge - info { background : rgba ( 79 , 179 , 217 , . 16 ) ; color : var ( - - info ) }
. wf - meta { display : grid ; grid - template - columns : repeat ( auto - fit , minmax ( 150 px , 1f r ) ) ; gap : 10 px 18 px ; margin : 16 px 0 0 ; padding : 14 px 0 0 ; border - top : 1 px dashed var ( - - border ) }
. wf - meta . m - label { font - size : 11 px ; text - transform : uppercase ; letter - spacing : . 04 em ; color : var ( - - muted ) }
. wf - meta . m - value { font - size : 14 px ; margin - top : 2 px }
. wf - trigger { margin - top : 14 px ; background : var ( - - panel2 ) ; border : 1 px solid var ( - - border ) ; border - radius : 8 px ; padding : 10 px 12 px }
. wf - trigger - title { font - size : 11 px ; text - transform : uppercase ; letter - spacing : . 04 em ; color : var ( - - muted ) ; margin - bottom : 4 px }
. wf - trigger code { color : var ( - - info ) }
. wf - sec { margin - top : 26 px }
. wf - sec - title { font - size : 17 px ; margin - bottom : 14 px }
. wf - empty { color : var ( - - muted ) ; background : var ( - - panel ) ; border : 1 px dashed var ( - - border ) ; border - radius : 8 px ; padding : 16 px }
. wf - flow { display : flex ; flex - direction : column }
. wf - step { display : flex ; gap : 12 px ; min - width : 0 }
. wf - rail { display : flex ; flex - direction : column ; align - items : center ; width : 22 px ; flex : 0 0 22 px }
. wf - dot { width : 14 px ; height : 14 px ; border - radius : 50 % ; background : var ( - - accent ) ; box - shadow : 0 0 0 3 px color - mix ( in srgb , var ( - - accent ) 22 % , transparent ) ; flex : 0 0 auto ; margin - top : 20 px }
. wf - step : first - child . wf - dot { background : var ( - - root ) ; box - shadow : 0 0 0 3 px rgba ( 253 , 230 , 138 , . 3 ) }
. wf - line { width : 2 px ; background : var ( - - border ) ; flex : 1 1 auto ; margin - top : 4 px }
. wf - card { flex : 1 ; background : var ( - - panel ) ; border : 1 px solid var ( - - border ) ; border - radius : 10 px ; padding : 12 px 14 px ; margin : 0 0 14 px ; cursor : pointer ; transition : box - shadow . 12 s , border - color . 12 s }
. wf - step : last - child . wf - card { margin - bottom : 0 }
. wf - card : hover { border - color : var ( - - accent ) ; box - shadow : 0 3 px 10 px rgba ( 0 , 0 , 0 , . 07 ) }
. wf - card - head { display : flex ; align - items : center ; gap : 8 px ; flex - wrap : wrap }
. wf - step - num { display : inline - flex ; align - items : center ; justify - content : center ; min - width : 22 px ; height : 22 px ; border - radius : 50 % ; background : var ( - - panel2 ) ; border : 1 px solid var ( - - border ) ; font - size : 12 px ; font - weight : 700 ; color : var ( - - muted ) }
. wf - key { font - weight : 700 ; font - size : 15 px }
. chip { display : inline - flex ; align - items : center ; padding : 1 px 8 px ; border - radius : 999 px ; font - size : 11 px ; font - weight : 600 ; border : 1 px solid var ( - - border ) ; background : var ( - - panel2 ) ; color : var ( - - muted ) }
. chip - mode { color : var ( - - accent ) ; border - color : color - mix ( in srgb , var ( - - accent ) 40 % , var ( - - border ) ) }
2026-09-09 18:40:35 +00:00
. chip - lang { color : var ( - - text ) ; padding : 0 4 px ; border - radius : 4 px }
2026-09-01 16:37:53 +00:00
. lang - javascript { background : # f7df1e ; color : # 1 c2330 }
. lang - typescript { background : # 3178 c6 ; color : # fff }
. lang - python { background : # 3776 ab ; color : # fff }
. lang - csharp { background : # 68217 a ; color : # fff }
. lang - shell { background : # 4 eaa25 ; color : # fff }
. lang - plaintext { background : var ( - - panel2 ) ; color : var ( - - muted ) }
. wf - card - detail { display : flex ; flex - wrap : wrap ; gap : 18 px ; margin - top : 10 px ; color : var ( - - muted ) }
. wf - card - detail . d - item { min - width : 120 px }
. wf - card - detail . d - label { font - size : 10 px ; text - transform : uppercase ; letter - spacing : . 05 em }
. wf - card - detail . d - value { font - size : 13 px ; margin - top : 2 px ; color : var ( - - text ) }
. wf - links { margin - top : 8 px ; display : flex ; flex - wrap : wrap ; gap : 8 px ; font - size : 12 px }
. wf - link { color : var ( - - muted ) }
. wf - link b { color : var ( - - accent ) ; font - weight : 600 }
. wf - envs { margin - top : 8 px ; display : flex ; flex - wrap : wrap ; gap : 6 px }
. wf - env { background : var ( - - panel2 ) ; border : 1 px solid var ( - - border ) ; border - radius : 6 px ; padding : 2 px 7 px ; font - size : 11.5 px }
. wf - env code { font - size : 11 px }
. wf - errors { display : flex ; flex - direction : column ; gap : 10 px }
. wf - err - card { display : flex ; align - items : center ; flex - wrap : wrap ; gap : 10 px ; background : rgba ( 229 , 106 , 90 , . 07 ) ; border : 1 px dashed color - mix ( in srgb , var ( - - error ) 55 % , var ( - - border ) ) ; border - radius : 10 px ; padding : 12 px 14 px }
. wf - err - src , . wf - err - dst { font - size : 13 px ; font - weight : 700 }
. wf - err - arrow { color : var ( - - error ) ; font - size : 12 px ; text - transform : uppercase ; letter - spacing : . 04 em }
. wf - err - lang { color : var ( - - muted ) ; font - size : 12 px }
. wf - table - wrapper { overflow - x : auto ; border : 1 px solid var ( - - border ) ; border - radius : 10 px ; background : var ( - - panel ) }
. wf - table { width : 100 % ; border - collapse : collapse ; min - width : 760 px ; font - size : 12.5 px }
. wf - table thead th { background : var ( - - panel2 ) ; text - align : left ; padding : 9 px 10 px ; font - size : 11 px ; text - transform : uppercase ; letter - spacing : . 04 em ; color : var ( - - muted ) ; border - bottom : 1 px solid var ( - - border ) ; position : sticky ; top : 0 }
. wf - table tbody td { padding : 8 px 10 px ; border - bottom : 1 px solid var ( - - border ) ; vertical - align : top }
. wf - table tbody tr : last - child td { border - bottom : none }
. wf - table tbody tr : hover { background : color - mix ( in srgb , var ( - - accent ) 6 % , transparent ) }
. wf - foot { margin - top : 26 px ; padding - top : 14 px ; border - top : 1 px solid var ( - - border ) ; color : var ( - - muted ) ; font - size : 12 px }
. wf - foot code { color : var ( - - muted ) }
";
// ------------------------------------------------------------- builders
private static void Badge ( StringBuilder sb , string label , string cls )
{
sb . Append ( "<span class=\"badge " ) . Append ( cls ) . Append ( "\">" ) . Append ( Html ( label ) ) . Append ( "</span>\n" ) ;
}
private static void MetaItem ( StringBuilder sb , string label , string value )
{
sb . Append ( "<div><div class=\"m-label\">" ) . Append ( Html ( label ) ) . Append ( "</div><div class=\"m-value\">" ) . Append ( Html ( value ) ) . Append ( "</div></div>\n" ) ;
}
private static void CardDetail ( StringBuilder sb , WorkflowTask task , IReadOnlyDictionary < Guid , WorkflowTask > byId )
{
var entry = EntryOf ( task ) ;
sb . Append ( "<div class=\"wf-card-detail\">\n" ) ;
DetailItem ( sb , "Entry file" , entry . File ? ? "—" ) ;
DetailItem ( sb , "Function" , string . IsNullOrWhiteSpace ( entry . Function ) ? "main()" : $"{entry.Function}()" ) ;
DetailItem ( sb , "Mode" , task . Mode ) ;
sb . Append ( "</div>\n" ) ;
var links = new List < string > ( ) ;
if ( task . NextId ! = null ) links . Add ( $"next <b>{Html(KeyById(task.NextId, byId) ?? " ? ")}</b>" ) ;
if ( task . OnErrorId ! = null ) links . Add ( $"onError <b>{Html(KeyById(task.OnErrorId, byId) ?? " ? ")}</b>" ) ;
if ( links . Count > 0 )
sb . Append ( "<div class=\"wf-links\">" ) . Append ( string . Join ( " · " , links ) ) . Append ( "</div>\n" ) ;
var env = EnvOf ( task ) ;
if ( env is { Count : > 0 } )
{
sb . Append ( "<div class=\"wf-envs\">\n" ) ;
foreach ( var kv in env . OrderBy ( x = > x . Key ) )
sb . Append ( "<span class=\"wf-env\"><code>" ) . Append ( Html ( kv . Key ) ) . Append ( "</code> = <code>" ) . Append ( Html ( kv . Value ) ) . Append ( "</code></span>\n" ) ;
sb . Append ( "</div>\n" ) ;
}
}
private static void DetailItem ( StringBuilder sb , string label , string value )
{
sb . Append ( "<div class=\"d-item\"><div class=\"d-label\">" ) . Append ( Html ( label ) ) . Append ( "</div><div class=\"d-value\">" ) . Append ( Html ( value ) ) . Append ( "</div></div>\n" ) ;
}
private static void Raw ( StringBuilder sb , string s ) = > sb . Append ( s ) ;
private static string Chip ( string label , string cls )
= > $"<span class=\" chip { cls } \ ">{Html(label)}</span>" ;
// ------------------------------------------------------------- data
private static List < WorkflowTask > HappyPathChain ( IReadOnlyList < WorkflowTask > tasks , IReadOnlyDictionary < Guid , WorkflowTask > byId )
{
var root = tasks . FirstOrDefault ( x = > x . Key = = "root" ) ? ? tasks . OrderBy ( x = > x . Order ) . FirstOrDefault ( ) ;
var chain = new List < WorkflowTask > ( ) ;
var visited = new HashSet < Guid > ( ) ;
var cur = root ;
while ( cur ! = null & & visited . Add ( cur . Id ) )
{
chain . Add ( cur ) ;
cur = cur . NextId ! = null & & byId . TryGetValue ( cur . NextId . Value , out var next ) ? next : null ;
}
return chain ;
}
private static string? KeyById ( Guid ? id , IReadOnlyDictionary < Guid , WorkflowTask > byId )
= > id ! = null & & byId . TryGetValue ( id . Value , out var t ) ? t . Key : null ;
private static EntryDefinition EntryOf ( WorkflowTask task )
= > JsonSerializer . Deserialize < EntryDefinition > ( task . EntryJson ? ? "{}" , Json ) ? ? new EntryDefinition ( ) ;
private static Dictionary < string , string > ? EnvOf ( WorkflowTask task )
= > JsonSerializer . Deserialize < Dictionary < string , string > > ( task . EnvJson ? ? "{}" , Json ) ;
private static string EnvSummary ( WorkflowTask task )
{
var env = EnvOf ( task ) ;
return env is { Count : > 0 } ? $"{env.Count} var" : "—" ;
}
private static TriggerDefinition ? ParseTrigger ( string? json )
= > string . IsNullOrWhiteSpace ( json ) ? null : JsonSerializer . Deserialize < TriggerDefinition > ( json , Json ) ;
private static string? FirstLanguage ( IReadOnlyList < WorkflowTask > tasks )
= > tasks . Select ( x = > x . Language ) . FirstOrDefault ( x = > ! string . IsNullOrWhiteSpace ( x ) ) ;
private static string? EntrySummary ( IReadOnlyList < WorkflowTask > tasks )
{
var root = tasks . FirstOrDefault ( x = > x . Key = = "root" ) ;
if ( root = = null ) return null ;
var e = EntryOf ( root ) ;
return $"{e.File ?? " — "} :: {e.Function ?? " main "}()" ;
}
private static string LangClass ( string? lang )
= > lang switch
{
"javascript" = > "javascript" ,
"typescript" = > "typescript" ,
"python" = > "python" ,
"csharp" = > "csharp" ,
"shell" = > "shell" ,
_ = > "plaintext" ,
} ;
private static string Html ( string? s ) = >
System . Net . WebUtility . HtmlEncode ( s ? ? string . Empty ) ;
}