2026-03-24 13:33:01 +01:00
|
|
|
namespace Fibble.FibLib
|
2026-03-27 10:58:18 +01:00
|
|
|
|
2026-03-24 13:33:01 +01:00
|
|
|
open System.Net
|
|
|
|
|
open FParsec
|
|
|
|
|
open YamlDotNet.Serialization
|
|
|
|
|
open System.Collections.Generic
|
|
|
|
|
|
2026-03-27 10:58:18 +01:00
|
|
|
|
2026-03-28 14:20:34 +01:00
|
|
|
// ==========================================
|
|
|
|
|
// 1. AST & Utils
|
|
|
|
|
// ==========================================
|
|
|
|
|
module Ast =
|
2026-03-31 13:36:22 +02:00
|
|
|
|
|
|
|
|
type Attr = { id: string; classes: string list; kvp: (string * string) list }
|
|
|
|
|
|
|
|
|
|
|
2026-03-28 14:20:34 +01:00
|
|
|
type InlineNode =
|
|
|
|
|
| Text of string
|
|
|
|
|
| RawHtml of string
|
2026-03-31 13:36:22 +02:00
|
|
|
| Emph of InlineNode list
|
|
|
|
|
| Underline of InlineNode list
|
|
|
|
|
| Strong of InlineNode list
|
|
|
|
|
| Strikeout of InlineNode list
|
|
|
|
|
| Superscript of InlineNode list
|
|
|
|
|
| Subscript of InlineNode list
|
|
|
|
|
| Link of attributes: Attr * target: Target
|
|
|
|
|
| Code of attributes: Attr * text: string
|
|
|
|
|
| Image of attributes: Attr * altText: InlineNode list * target: Url
|
|
|
|
|
| Note of BlockNode list
|
|
|
|
|
| SoftBreak
|
|
|
|
|
| LineBreak
|
2026-03-28 14:20:34 +01:00
|
|
|
| Expr of code: string * result: string option
|
2026-03-31 13:36:22 +02:00
|
|
|
| Command of tag: string * args: string list * kwargs: Map<string, string> * children: InlineNode list
|
|
|
|
|
and BlockNode =
|
|
|
|
|
| CodeBlock of attributes: Attr * text: string
|
|
|
|
|
| Figure of attributes: Attr * caption: InlineNode list * blocks: BlockNode list
|
|
|
|
|
| ListBlock of ListKind
|
|
|
|
|
| Plain of InlineNode list
|
2026-03-28 14:20:34 +01:00
|
|
|
| Paragraph of children: InlineNode list
|
2026-03-31 13:36:22 +02:00
|
|
|
| Section of level: int * args: (string * string) list * children: InlineNode list
|
|
|
|
|
and ListKind =
|
|
|
|
|
| Orderedlist of attributes: Attr * start: int * blocksList: (BlockNode list) list
|
|
|
|
|
| BulletList of attributes: Attr * blocksList: (BlockNode list) list
|
|
|
|
|
and Url = string
|
|
|
|
|
and Target = Url * InlineNode list
|
2026-03-28 14:20:34 +01:00
|
|
|
|
|
|
|
|
type Document = BlockNode list
|
|
|
|
|
|
2026-03-31 13:36:22 +02:00
|
|
|
type TagRenderer = Map<string,string>
|
|
|
|
|
-> string list
|
|
|
|
|
-> Map<string,string>
|
|
|
|
|
-> InlineNode list -> InlineNode
|
2026-03-28 14:20:34 +01:00
|
|
|
|
|
|
|
|
let rec stringifyNodes (nodes: InlineNode list) =
|
|
|
|
|
nodes
|
|
|
|
|
|> List.map (function
|
|
|
|
|
| Text t -> t
|
|
|
|
|
| RawHtml h -> h
|
|
|
|
|
| Expr(_, Some res) -> res
|
|
|
|
|
| Expr(code, None) -> sprintf "@(%s)" code // Fallback om den inte evaluerats
|
2026-03-31 13:36:22 +02:00
|
|
|
| _ -> failwith "haha"
|
2026-03-28 14:20:34 +01:00
|
|
|
)
|
|
|
|
|
|> String.concat ""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-03-27 10:58:18 +01:00
|
|
|
module Utils =
|
2026-03-28 14:20:34 +01:00
|
|
|
open Ast
|
2026-03-27 10:58:18 +01:00
|
|
|
let smartDedent (input: string) =
|
2026-03-27 12:17:36 +01:00
|
|
|
let lines = input.Replace("\r\n", "\n").Split '\n' |> List.ofArray
|
2026-03-27 10:58:18 +01:00
|
|
|
|
|
|
|
|
// 1. Hitta den minsta indenteringen bland alla rader som har text
|
|
|
|
|
let minIndent =
|
|
|
|
|
lines
|
|
|
|
|
|> List.filter (fun l -> not (System.String.IsNullOrWhiteSpace(l)))
|
|
|
|
|
|> List.map (fun l -> l.Length - l.TrimStart().Length)
|
|
|
|
|
|> function
|
|
|
|
|
| [] -> 0
|
|
|
|
|
| indents -> List.min indents
|
|
|
|
|
|
|
|
|
|
// 2. Dra av exakt så många mellanslag från alla rader
|
2026-03-27 12:17:36 +01:00
|
|
|
// för att dessa inte ska vara med i det slutgiltiga dokumentet.
|
2026-03-27 10:58:18 +01:00
|
|
|
let dedented =
|
|
|
|
|
lines
|
|
|
|
|
|> List.map (fun l ->
|
|
|
|
|
if System.String.IsNullOrWhiteSpace(l) then ""
|
|
|
|
|
else l.Substring(minIndent)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// 3. Slå ihop och städa bort överflödiga radbrytningar i början/slutet
|
|
|
|
|
(String.concat "\n" dedented).Trim('\n', '\r')
|
|
|
|
|
|
2026-03-28 14:20:34 +01:00
|
|
|
let positional f: TagRenderer =
|
2026-03-31 13:36:22 +02:00
|
|
|
fun _ (args: string list) _ children -> f args children
|
|
|
|
|
|
|
|
|
|
let onlyArgs f =
|
|
|
|
|
fun _ args kwargs children -> f args kwargs
|
2026-03-24 13:33:01 +01:00
|
|
|
|
2026-03-28 14:20:34 +01:00
|
|
|
let getArgIdx (args: (string*string) list) index defaultVal =
|
|
|
|
|
let unnamed = args |> List.filter (fun (k, _) -> k = "")
|
|
|
|
|
if index < unnamed.Length then (snd unnamed.[index]).Trim('"')
|
|
|
|
|
else defaultVal
|
2026-03-24 13:33:01 +01:00
|
|
|
|
2026-03-28 14:20:34 +01:00
|
|
|
// This gets the arg defined by "key" unless it is not set, it then tries to get it by index. If that fails, it gets the defaultVal
|
|
|
|
|
let getArg (args: (string * string) list) (key: string) (index: int) (defaultVal: string) =
|
|
|
|
|
match args |> List.tryFind (fun (k, _) -> k = key) with
|
|
|
|
|
| Some (_, v) -> v.Trim('"')
|
|
|
|
|
| None -> getArgIdx args index defaultVal
|
|
|
|
|
|
|
|
|
|
let withArg1 def (f: string -> InlineNode list -> InlineNode) =
|
|
|
|
|
fun _ args children -> f (getArgIdx args 0 def) children
|
2026-03-24 13:33:01 +01:00
|
|
|
|
2026-03-28 14:20:34 +01:00
|
|
|
let withArg2 (k1: string) (d1: string) (k2: string) (d2: string) (f: string -> string -> InlineNode list -> InlineNode) =
|
|
|
|
|
fun _ args children -> f (getArg args k1 0 d1) (getArg args k2 1 d2) children
|
|
|
|
|
|
2026-03-27 12:17:36 +01:00
|
|
|
|
2026-03-27 10:58:18 +01:00
|
|
|
|
|
|
|
|
|
2026-03-24 13:33:01 +01:00
|
|
|
// ==========================================
|
2026-03-27 10:58:18 +01:00
|
|
|
// 2. Parser
|
2026-03-24 13:33:01 +01:00
|
|
|
// ==========================================
|
2026-03-27 10:58:18 +01:00
|
|
|
|
2026-03-24 13:33:01 +01:00
|
|
|
module Parser =
|
|
|
|
|
open Ast
|
|
|
|
|
|
|
|
|
|
let pInline, pInlineRef = createParserForwardedToRef<InlineNode, unit>()
|
|
|
|
|
|
|
|
|
|
let getSectionLevel (name: string) =
|
|
|
|
|
if name = "section" then 1
|
|
|
|
|
elif name = "subsection" then 2
|
|
|
|
|
elif name.StartsWith("sub") && name.EndsWith("section") then (name.Length - 7) / 3 + 1
|
|
|
|
|
else 1
|
|
|
|
|
|
|
|
|
|
let isSection (name: string) = name.EndsWith("section")
|
|
|
|
|
|
2026-03-27 10:58:18 +01:00
|
|
|
let pNewline = newline
|
2026-03-31 13:36:22 +02:00
|
|
|
|
|
|
|
|
let pNamedArg =
|
|
|
|
|
// Leta efter "nyckel=värde"
|
|
|
|
|
attempt (many1Chars (asciiLetter <|> digit <|> anyOf "-_")
|
|
|
|
|
.>> spaces .>> pchar '=' .>> spaces)
|
|
|
|
|
.>>. manyChars (noneOf ",]")
|
|
|
|
|
|>> fun (k, v) -> (k, v.Trim())
|
|
|
|
|
|
|
|
|
|
let pPositionalArg =
|
|
|
|
|
// Bara "värde"
|
|
|
|
|
manyChars (noneOf ",]") |>> fun v -> ("", v.Trim())
|
|
|
|
|
|
|
|
|
|
let pSingleArg = spaces >>. (pNamedArg <|> pPositionalArg) .>> spaces
|
|
|
|
|
|
|
|
|
|
let pArgs =
|
|
|
|
|
between (pstring "[") (pstring "]") (sepBy pSingleArg (pchar ','))
|
|
|
|
|
>>= fun args ->
|
|
|
|
|
// Validera att positionella argument alltid kommer först
|
|
|
|
|
let rec validate canBePositional = function
|
|
|
|
|
| [] -> preturn args // Allt är okej, returnera listan
|
|
|
|
|
| ("", _) :: tail ->
|
|
|
|
|
if not canBePositional then
|
|
|
|
|
fail "Syntaxfel: Positionella argument får inte komma efter namngivna argument."
|
|
|
|
|
else validate true tail
|
|
|
|
|
| _ :: tail ->
|
|
|
|
|
validate false tail // Vi hittade ett namngivet argument, inga fler positionella tillåts
|
|
|
|
|
|
|
|
|
|
validate true args
|
|
|
|
|
// let pArg =
|
|
|
|
|
// spaces >>.
|
|
|
|
|
// choice [
|
|
|
|
|
// attempt (many1Chars (asciiLetter <|> digit <|> anyOf "-_")
|
|
|
|
|
// .>> spaces .>> pchar '=' .>> spaces
|
|
|
|
|
// .>>. manyChars (noneOf ",]"))
|
|
|
|
|
// |>> fun (k, v) -> (k, v.Trim())
|
|
|
|
|
//
|
|
|
|
|
// // Fallback: bara "värde" (ges en tom nyckel)
|
|
|
|
|
// manyChars (noneOf ",]") |>> fun v -> ("", v.Trim())
|
|
|
|
|
// ] .>> spaces
|
|
|
|
|
// let pArgs = between (pstring "[") (pstring "]") (sepBy pArg (pstring ","))
|
2026-03-24 13:33:01 +01:00
|
|
|
|
2026-03-27 10:58:18 +01:00
|
|
|
// --- 1. Måsvinge-parser (för @kommandon) ---
|
|
|
|
|
let pRawBody, pRawBodyRef = createParserForwardedToRef<string, unit>()
|
|
|
|
|
pRawBodyRef.Value <-
|
|
|
|
|
many (choice [
|
|
|
|
|
many1Chars (noneOf "{}")
|
|
|
|
|
pchar '{' >>. pRawBodyRef.Value .>> pchar '}' |>> sprintf "{%s}"
|
|
|
|
|
]) |>> String.concat ""
|
|
|
|
|
|
2026-03-28 14:20:34 +01:00
|
|
|
let pBody =
|
2026-03-27 10:58:18 +01:00
|
|
|
between (pstring "{") (pstring "}") pRawBodyRef.Value >>= fun raw ->
|
2026-03-28 14:20:34 +01:00
|
|
|
// En mer tillåtande parser isolerad för innehållet inuti {...}
|
|
|
|
|
let pInnerInline =
|
|
|
|
|
choice [
|
|
|
|
|
attempt pInline
|
|
|
|
|
// Fångar upp dubbla radbrytningar och måsvingar som pInline normalt blockerar
|
|
|
|
|
many1Chars (anyOf "\r\n{}") |>> Text
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
match run (many pInnerInline .>> eof) (Utils.smartDedent raw) with
|
2026-03-27 10:58:18 +01:00
|
|
|
| Success(n, _, _) -> preturn n
|
|
|
|
|
| Failure(m, _, _) -> fail m
|
|
|
|
|
|
|
|
|
|
// --- 2. Parentes-parser (för @(...) med sträng-stöd) ---
|
|
|
|
|
let pParenBody, pParenBodyRef = createParserForwardedToRef<string, unit>()
|
|
|
|
|
|
|
|
|
|
// En inre parser som känner igen F#-strängar och escape-tecken (\")
|
|
|
|
|
let pFSharpString =
|
|
|
|
|
let normal = many1Chars (noneOf "\"\\")
|
|
|
|
|
let escaped = pstring "\\" >>. anyChar |>> sprintf "\\%c"
|
|
|
|
|
pstring "\"" .>>. manyStrings (normal <|> escaped) .>>. pstring "\""
|
|
|
|
|
|>> fun ((start, inner), end_) -> start + inner + end_
|
|
|
|
|
|
|
|
|
|
// Själva loopen letar nu efter strängar FÖRST, sen vanlig text, och sist inre parenteser
|
|
|
|
|
pParenBodyRef.Value <-
|
|
|
|
|
manyStrings (choice [
|
|
|
|
|
pFSharpString
|
|
|
|
|
many1Chars (noneOf "()\"")
|
|
|
|
|
pchar '(' >>. pParenBody .>> pchar ')' |>> sprintf "(%s)"
|
|
|
|
|
])
|
|
|
|
|
|
|
|
|
|
let pExpr =
|
|
|
|
|
attempt (pstring "@(") >>. pParenBody .>> pstring ")"
|
|
|
|
|
|>> fun c -> Expr(c, None)
|
|
|
|
|
|
|
|
|
|
// --- Övriga inline-parsers ---
|
|
|
|
|
let pMultilineCode =
|
|
|
|
|
pstring "@\"\"\"" >>. manyCharsTill anyChar (pstring "\"\"\"")
|
|
|
|
|
|>> fun c -> Expr(Utils.smartDedent c, None)
|
|
|
|
|
|
2026-03-31 13:36:22 +02:00
|
|
|
// let pInlineCommand =
|
|
|
|
|
// attempt (
|
|
|
|
|
// pchar '@' >>. many1Chars asciiLetter >>= fun name ->
|
|
|
|
|
// if isSection name then fail "Sektioner är block-element."
|
|
|
|
|
// else preturn name
|
|
|
|
|
// )
|
|
|
|
|
// .>>. opt pArgs
|
|
|
|
|
// .>>. opt pBody
|
|
|
|
|
// |>> fun ((n, a), b) -> Command(n, defaultArg a [], defaultArg b [])
|
2026-03-24 13:33:01 +01:00
|
|
|
let pInlineCommand =
|
2026-03-31 13:36:22 +02:00
|
|
|
attempt (pchar '@' >>. many1Chars asciiLetter)
|
2026-03-24 13:33:01 +01:00
|
|
|
.>>. opt pArgs
|
2026-03-27 10:58:18 +01:00
|
|
|
.>>. opt pBody
|
2026-03-31 13:36:22 +02:00
|
|
|
|>> fun ((name, argsOpt), bodyOpt) ->
|
|
|
|
|
// Hämta den råa tupel-listan från parsern (eller en tom lista om inga argument angavs)
|
|
|
|
|
let rawArgs = defaultArg argsOpt []
|
|
|
|
|
|
|
|
|
|
// 1. Filtrera fram positionella argument (de som har en tom nyckel) och plocka ut värdet
|
|
|
|
|
let posArgs =
|
|
|
|
|
rawArgs
|
|
|
|
|
|> List.choose (fun (k, v) -> if k = "" then Some v else None)
|
|
|
|
|
|
|
|
|
|
// 2. Filtrera fram namngivna argument och gör om dem till en Map
|
|
|
|
|
let kwargs =
|
|
|
|
|
rawArgs
|
|
|
|
|
|> List.filter (fun (k, _) -> k <> "")
|
|
|
|
|
|> Map.ofList
|
|
|
|
|
|
|
|
|
|
// Skapa din nya Command-nod!
|
|
|
|
|
Command(name, posArgs, kwargs, defaultArg bodyOpt [])
|
2026-03-27 10:58:18 +01:00
|
|
|
// MÅSTE tilldelas efter att alla pExpr, pInlineCommand etc. är definierade
|
|
|
|
|
pInlineRef.Value <- choice [
|
|
|
|
|
pMultilineCode
|
|
|
|
|
pExpr
|
|
|
|
|
pInlineCommand
|
|
|
|
|
many1Chars (noneOf "@{}\n\r") |>> Text
|
|
|
|
|
attempt (pNewline .>> notFollowedBy pNewline) |>> fun _ -> Text "\n"
|
|
|
|
|
pchar '@' |>> fun _ -> Text "@"
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
// --- Block Parsers ---
|
|
|
|
|
let pSectionBlock =
|
|
|
|
|
attempt (
|
|
|
|
|
pchar '@' >>. many1Chars asciiLetter >>= fun name ->
|
|
|
|
|
if isSection name then preturn name
|
|
|
|
|
else fail "Inte en sektion."
|
|
|
|
|
)
|
|
|
|
|
.>>. opt pArgs
|
2026-03-24 13:33:01 +01:00
|
|
|
.>>. opt pBody
|
|
|
|
|
|>> fun ((name, argsOpt), bodyOpt) ->
|
2026-03-27 10:58:18 +01:00
|
|
|
Section(getSectionLevel name, defaultArg argsOpt [], defaultArg bodyOpt [])
|
|
|
|
|
|
|
|
|
|
let pParagraphBlock = many1 pInline |>> Paragraph
|
|
|
|
|
|
|
|
|
|
let pBlock = choice [ pSectionBlock; pParagraphBlock ]
|
|
|
|
|
|
|
|
|
|
// --- Dokument Parser ---
|
|
|
|
|
let pDocument =
|
|
|
|
|
spaces
|
|
|
|
|
>>. opt (
|
|
|
|
|
pstring "---"
|
|
|
|
|
>>. manyCharsTill anyChar (attempt (pNewline >>. pstring "---"))
|
|
|
|
|
|>> fun yamlStr ->
|
|
|
|
|
let deserializer = DeserializerBuilder().Build()
|
|
|
|
|
let dict = deserializer.Deserialize<Dictionary<string, string>>(yamlStr)
|
|
|
|
|
if isNull dict then Map.empty
|
|
|
|
|
else dict |> Seq.map (fun kvp -> kvp.Key, kvp.Value) |> Map.ofSeq
|
|
|
|
|
)
|
|
|
|
|
.>> spaces
|
|
|
|
|
.>>. sepEndBy pBlock (many1 pNewline)
|
|
|
|
|
.>> eof
|
|
|
|
|
|>> fun (headerOpt, blocks) ->
|
|
|
|
|
(defaultArg headerOpt Map.empty, blocks)
|
|
|
|
|
|
|
|
|
|
let parse i =
|
|
|
|
|
match run pDocument i with
|
|
|
|
|
| Success(r, _, _) -> r
|
|
|
|
|
| Failure(e, _, _) -> failwith e
|
|
|
|
|
|
|
|
|
|
|
2026-03-24 13:33:01 +01:00
|
|
|
// ==========================================
|
2026-03-27 10:58:18 +01:00
|
|
|
// 3. Execution & Printer
|
2026-03-24 13:33:01 +01:00
|
|
|
// ==========================================
|
|
|
|
|
type IEvaluator =
|
2026-03-27 10:58:18 +01:00
|
|
|
abstract member Evaluate: string -> string
|
2026-03-24 13:33:01 +01:00
|
|
|
|
|
|
|
|
module Execution =
|
|
|
|
|
open Ast
|
|
|
|
|
|
2026-03-27 12:17:36 +01:00
|
|
|
let rec transform (metadata: Map<string, string>) (prelude: Map<string, TagRenderer>) (eval: IEvaluator) = function
|
2026-03-31 13:36:22 +02:00
|
|
|
| Command(name, args, kwargs, children) when prelude.ContainsKey name ->
|
|
|
|
|
prelude.[name] metadata args kwargs (children |> List.map (transform metadata prelude eval))
|
|
|
|
|
| Command(n, _, _ ,_) -> failwithf "%s is not a defined command" n
|
2026-03-27 12:17:36 +01:00
|
|
|
| Expr(c, _) -> RawHtml (eval.Evaluate c)
|
2026-03-27 10:58:18 +01:00
|
|
|
| n -> n
|
2026-03-24 13:33:01 +01:00
|
|
|
|
2026-03-31 13:36:22 +02:00
|
|
|
|
2026-03-27 10:58:18 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
|
module Evaluators =
|
|
|
|
|
open System.IO
|
|
|
|
|
open System.Text
|
|
|
|
|
open FSharp.Compiler.Interactive.Shell
|
|
|
|
|
|
|
|
|
|
type FsiEvaluator() =
|
2026-03-31 13:36:22 +02:00
|
|
|
let sbOut = StringBuilder()
|
|
|
|
|
let sbErr = StringBuilder()
|
2026-03-27 10:58:18 +01:00
|
|
|
let inStream = new StringReader("")
|
|
|
|
|
let outStream = new StringWriter(sbOut)
|
|
|
|
|
let errStream = new StringWriter(sbErr)
|
|
|
|
|
|
|
|
|
|
// Initiera FSI-sessionen
|
|
|
|
|
let fsiConfig = FsiEvaluationSession.GetDefaultConfiguration()
|
|
|
|
|
let argv = [| "fsi.exe"; "--noninteractive" |]
|
|
|
|
|
let session = FsiEvaluationSession.Create(fsiConfig, argv, inStream, outStream, errStream)
|
|
|
|
|
|
|
|
|
|
interface IEvaluator with
|
|
|
|
|
member _.Evaluate(code: string) =
|
|
|
|
|
sbOut.Clear() |> ignore
|
|
|
|
|
sbErr.Clear() |> ignore
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
let result, _warnings = session.EvalInteractionNonThrowing(code)
|
|
|
|
|
|
|
|
|
|
let output = sbOut.ToString()
|
|
|
|
|
let errors = sbErr.ToString().Trim()
|
|
|
|
|
|
|
|
|
|
// Filtrera bort FSI:s automatiska typsignaturer från utskriften
|
|
|
|
|
let cleanOutput =
|
|
|
|
|
output.Replace("\r\n", "\n").Split('\n')
|
|
|
|
|
|> Array.filter (fun line ->
|
|
|
|
|
let l = line.TrimStart()
|
|
|
|
|
not (l.StartsWith("val ") || l.StartsWith("type ") || l.StartsWith("module ") || l.StartsWith("namespace "))
|
|
|
|
|
)
|
|
|
|
|
|> String.concat "\n"
|
|
|
|
|
|> fun s -> s.Trim()
|
|
|
|
|
|
|
|
|
|
match result with
|
|
|
|
|
| Choice1Of2 (Some fsiValue) ->
|
|
|
|
|
// Plocka ut värdet. Om det är en sträng, undvik "%A" för att slippa citattecken.
|
|
|
|
|
let valStr =
|
|
|
|
|
if isNull fsiValue.ReflectionValue then ""
|
|
|
|
|
else
|
|
|
|
|
match fsiValue.ReflectionValue with
|
|
|
|
|
| :? string as s -> s
|
|
|
|
|
| v -> sprintf "%A" v
|
|
|
|
|
|
|
|
|
|
if System.String.IsNullOrEmpty(cleanOutput) then valStr
|
|
|
|
|
elif System.String.IsNullOrEmpty(valStr) then cleanOutput
|
|
|
|
|
else cleanOutput + "\n" + valStr
|
|
|
|
|
|
|
|
|
|
| Choice1Of2 None ->
|
|
|
|
|
if not (System.String.IsNullOrEmpty(errors)) then
|
|
|
|
|
cleanOutput + sprintf "%A" errors
|
|
|
|
|
else
|
|
|
|
|
cleanOutput
|
|
|
|
|
|
|
|
|
|
| Choice2Of2 ex ->
|
|
|
|
|
let fsiErrorOutput = if System.String.IsNullOrEmpty(errors) then "Ingen ytterligare FSI-output." else errors
|
|
|
|
|
sprintf """
|
|
|
|
|
<div style="color: #721c24; background-color: #f8d7da; border-color: #f5c6cb; padding: 10px; margin-bottom: 10px; border-radius: 5px;">
|
|
|
|
|
<strong>FSI Exekveringsfel!</strong><br/>
|
|
|
|
|
<strong>Kod som kördes:</strong> <code>%s</code><br/><br/>
|
|
|
|
|
<strong>Exception:</strong> %s<br/>
|
|
|
|
|
<strong>FSI Stderr:</strong> <pre style="margin:0; background: rgba(255,255,255,0.5); padding: 5px;">%s</pre>
|
|
|
|
|
</div>""" code ex.Message fsiErrorOutput
|
|
|
|
|
|
|
|
|
|
with ex ->
|
|
|
|
|
sprintf """<div style="color: red; border: 1px solid red; padding: 10px;">
|
|
|
|
|
<strong>Kritiskt FSI-systemfel:</strong> %s
|
|
|
|
|
</div>""" ex.Message
|
|
|
|
|
|
2026-03-27 12:17:36 +01:00
|
|
|
|