From 14697b3dd9817ef24d960c57254fcc3b2ddacbcb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?linus=20bj=C3=B6rnstam?= Date: Fri, 27 Mar 2026 12:17:36 +0100 Subject: [PATCH 1/2] added metadata --- src/FibLib/Library.fs | 213 +++--------------------------------------- src/Fibble/Program.fs | 41 ++++---- 2 files changed, 31 insertions(+), 223 deletions(-) diff --git a/src/FibLib/Library.fs b/src/FibLib/Library.fs index e1f6c71..a7af3c6 100644 --- a/src/FibLib/Library.fs +++ b/src/FibLib/Library.fs @@ -8,7 +8,7 @@ open System.Collections.Generic module Utils = let smartDedent (input: string) = - let lines = input.Replace("\r\n", "\n").Split('\n') |> List.ofArray + let lines = input.Replace("\r\n", "\n").Split '\n' |> List.ofArray // 1. Hitta den minsta indenteringen bland alla rader som har text let minIndent = @@ -20,6 +20,7 @@ module Utils = | indents -> List.min indents // 2. Dra av exakt så många mellanslag från alla rader + // för att dessa inte ska vara med i det slutgiltiga dokumentet. let dedented = lines |> List.map (fun l -> @@ -29,30 +30,7 @@ module Utils = // 3. Slå ihop och städa bort överflödiga radbrytningar i början/slutet (String.concat "\n" dedented).Trim('\n', '\r') - -module Utils3 = - let smartDedent (input: string) = - let text = input.Trim('\n', '\r') - let lines = text.Replace("\r\n", "\n").Split('\n') |> List.ofArray - - let rec loop lines currentBase acc = - match lines with - | [] -> List.rev acc |> String.concat "\n" - | line :: tail -> - if System.String.IsNullOrWhiteSpace(line) then - loop tail currentBase ("" :: acc) - else - let indent = line.Length - line.TrimStart().Length - - if indent = 0 then - loop tail 0 (line :: acc) - else - let newBase = if currentBase = 0 then indent else currentBase - let toStrip = min newBase indent - let stripped = line.Substring toStrip - loop tail newBase (stripped :: acc) - - loop lines 0 [] + // ========================================== // 1. AST & Utils @@ -71,26 +49,10 @@ module Ast = type Document = BlockNode list - type TagRenderer = string list -> InlineNode list -> InlineNode + type TagRenderer = Map -> string list -> InlineNode list -> InlineNode -module Utils2 = - let smartDedent (input: string) = - let text = input.Trim('\n', '\r') - let lines = text.Replace("\r\n", "\n").Split '\n' |> List.ofArray - let rec loop lines currentBase acc = - match lines with - | [] -> List.rev acc |> String.concat "\n" - | line :: tail -> - if System.String.IsNullOrWhiteSpace line then - loop tail currentBase ("" :: acc) - else - let indent = line.Length - line.TrimStart().Length - let newBase = if currentBase = 0 then indent else currentBase - let toStrip = min newBase indent - loop tail newBase (line.Substring toStrip :: acc) - loop lines 0 [] // ========================================== // 2. Parser @@ -121,7 +83,7 @@ module Parser = pchar '{' >>. pRawBodyRef.Value .>> pchar '}' |>> sprintf "{%s}" ]) |>> String.concat "" - let pBody = + let pBody: Parser = between (pstring "{") (pstring "}") pRawBodyRef.Value >>= fun raw -> match run (many pInline .>> eof) (Utils.smartDedent raw) with | Success(n, _, _) -> preturn n @@ -214,112 +176,6 @@ module Parser = | Failure(e, _, _) -> failwith e -module Parser2 = - open Ast - - let pInline, pInlineRef = createParserForwardedToRef() - - // --- De saknade hjälpfunktionerna --- - 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" - - let pNewline = newline - - let pArg = spaces >>. manyChars (noneOf ",]") .>> spaces - let pArgs = between (pstring "[") (pstring "]") (sepBy pArg (pstring ",")) - // ------------------------------------ - - let pRawBody, pRawBodyRef = createParserForwardedToRef() - pRawBodyRef.Value <- - many (choice [ - many1Chars (noneOf "{}") - pchar '{' >>. pRawBodyRef.Value .>> pchar '}' |>> sprintf "{%s}" - ]) |>> String.concat "" - - let pBody = - between (pstring "{") (pstring "}") pRawBodyRef.Value >>= fun raw -> - match run (many pInline .>> eof) (Utils.smartDedent raw) with - | Success(n, _, _) -> preturn n - | Failure(m, _, _) -> fail m - - let pMultilineCode = - pstring "@\"\"\"" >>. manyCharsTill anyChar (pstring "\"\"\"") - |>> fun c -> Expr(Utils.smartDedent c, None) - - let pParenBody, pParenBodyRef = createParserForwardedToRef() - pParenBodyRef.Value <- - many (choice [ - many1Chars (noneOf "()") - pchar '(' >>. pParenBodyRef.Value .>> pchar ')' |>> sprintf "(%s)" - ]) |>> String.concat "" - - // let pExpr2 = - // attempt (pstring "@(") >>. manyChars (noneOf ")") .>> pstring ")" - // |>> fun c -> Expr(c, None) - - let pExpr = - attempt (pstring "@(") >>. pParenBodyRef.Value .>> pstring ")" - |>> fun c -> Expr(c, None) - - 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) -> Element(n, defaultArg a [], defaultArg b []) - - pInlineRef.Value <- choice [ - pMultilineCode - pExpr - pInlineCommand - many1Chars (noneOf "@{}\n\r") |>> Text - attempt (pNewline .>> notFollowedBy pNewline) |>> fun _ -> Text "\n" - ] - - let pSectionBlock = - attempt ( - pchar '@' >>. many1Chars asciiLetter >>= fun name -> - if isSection name then preturn name - else fail "Inte en sektion." - ) - .>>. opt pArgs - .>>. opt pBody - |>> fun ((name, argsOpt), bodyOpt) -> - Section(getSectionLevel name, defaultArg argsOpt [], defaultArg bodyOpt []) - - let pParagraphBlock = many1 pInline |>> Paragraph - - let pBlock = choice [ pSectionBlock; pParagraphBlock ] - - let pDocument = - spaces - >>. opt ( - pstring "---" - // HÄR ÄR FIXEN: attempt runt pNewline och pstring - >>. manyCharsTill anyChar (attempt (pNewline >>. pstring "---")) - |>> fun yamlStr -> - let deserializer = DeserializerBuilder().Build() - let dict = deserializer.Deserialize>(yamlStr) - if isNull dict then Map.empty - else dict |> Seq.map (fun kvp -> kvp.Key, kvp.Value) |> Map.ofSeq - ) - .>> spaces - .>>. sepEndBy pBlock (many1 pNewline) - |>> fun (headerOpt, blocks) -> - (defaultArg headerOpt Map.empty, blocks) - - let parse i = - match run pDocument i with - | Success(r, _, _) -> r - | Failure(e, _, _) -> failwith e // ========================================== // 3. Execution & Printer // ========================================== @@ -329,11 +185,13 @@ type IEvaluator = module Execution = open Ast - let rec transform (prelude: Map) (eval: IEvaluator) = - function - | Element(n, a, c) when prelude.ContainsKey n -> prelude.[n] a (c |> List.map (transform prelude eval)) - | Element(n, a, c) -> Element(n, a, c |> List.map (transform prelude eval)) - | Expr(c, _) -> RawHtml(eval.Evaluate c) + let rec transform (metadata: Map) (prelude: Map) (eval: IEvaluator) = function + | Element(n, a, c) when prelude.ContainsKey n -> + // Skicka in metadata som första argument till din funktion + prelude.[n] metadata a (c |> List.map (transform metadata prelude eval)) + | Element(n, a, c) -> + Element(n, a, c |> List.map (transform metadata prelude eval)) + | Expr(c, _) -> RawHtml (eval.Evaluate c) | n -> n module HtmlPrinter = @@ -427,49 +285,4 @@ module Evaluators = Kritiskt FSI-systemfel: %s """ ex.Message - // interface IEvaluator with - // member _.Evaluate(code: string) = - // sbOut.Clear() |> ignore - // sbErr.Clear() |> ignore - - // try - // printfn "%s" code - - // // Kör koden i FSI - // let result, _warnings = session.EvalInteractionNonThrowing code - - // // Läs av vad kompilatorn spottade ut - // let output = sbOut.ToString().Trim() - // let errors = sbErr.ToString().Trim() - - // match result with - // | Choice1Of2 (Some fsiValue) -> - // // Det gick bra och koden returnerade ett värde - // let valStr = sprintf "%A" fsiValue.ReflectionValue - // if System.String.IsNullOrEmpty(output) then valStr - // else output + "\n" + valStr - - // | Choice1Of2 None -> - // // Det gick bra, men koden returnerade inget värde (t.ex. en let-bindning) - // // Om det fanns utskrifter i sbErr (t.ex. varningar), kan vi visa dem här: - // if not (System.String.IsNullOrEmpty(errors)) then - // output + sprintf "\n %A" errors - // else - // output - - // | Choice2Of2 ex -> - // // FSI kastade ett exception (t.ex. syntaxfel eller runtime-fel) - // let fsiErrorOutput = if System.String.IsNullOrEmpty(errors) then "Ingen ytterligare FSI-output." else errors - // sprintf """ - //
- // FSI Exekveringsfel!
- // Kod som kördes: %s

- // Exception: %s
- // FSI Stderr:
%s
- //
""" code ex.Message fsiErrorOutput - - // with ex -> - // // Fångar upp om själva anropet till session.EvalInteractionNonThrowing kraschar helt - // sprintf """
- // Kritiskt FSI-systemfel: %s - //
""" ex.Message + diff --git a/src/Fibble/Program.fs b/src/Fibble/Program.fs index 3824277..f92e40c 100644 --- a/src/Fibble/Program.fs +++ b/src/Fibble/Program.fs @@ -7,41 +7,36 @@ open Fibble.FibLib.Ast // Ger oss tillgång till Element, Text, RawHtml etc. // ========================================== // makeElem returnerar nu en strukturerad Element-nod istället för en sträng -let makeElem (name : string) = - (name, fun args children -> Element(name, args, children)) +let elem (name : string) = + (name, fun meta args children -> Element(name, args, children)) let makeElems lst = - List.map makeElem lst + List.map elem lst -let defaultPrelude : Map = - [ "b"; "em"; "i"; "strong" ] - |> makeElems - |> Map.ofList -let myPrelude : Map = - Map.ofList [ - "quotient", fun args _ -> +let myPrelude : Map = + Map [ + "quotient", fun _ args _ -> if args.Length >= 2 then Text (sprintf "%d" (int args.[0] / int args.[1])) else Text "[Fel: quotient kräver två argument]" - - "bold", fun args children -> + "bold", fun _ args children -> Element("b", args, children) - - "link", fun args children -> + "value", fun meta args _ -> + Text meta[args.Head] + "link", fun _ args children -> let url = if args.Length > 0 then args.[0].Trim('"') else "#" - // Vi mappar argumenten till HTML-attribut (t.ex. href="...") Element("a", [sprintf "href=\"%s\"" url], children) - + // @br har varken argument eller barn, så vi returnerar bara rå HTML direkt - "br", fun _ _ -> - RawHtml "
" - ] + "br", fun _ _ _ -> RawHtml "
" + ] // ========================================== // 2. Mall och Evaluator // ========================================== + let pageTemplate = """ @@ -119,7 +114,7 @@ let increment () = Första anropet: @(increment()) Andra anropet: @(increment()) -Test av utskrift: +Test av utskrift: @value[date] " // ========================================== // 3. Huvudpipeline @@ -128,7 +123,7 @@ let processDocument (source: string) = let evaluator = Evaluators.FsiEvaluator() // Steg 1: Parsa koden - let (metadata, rawBlocks) = Parser.parse source + let metadata, rawBlocks = Parser.parse source // Hjälpfunktion: Gå igenom trädet och byt ut @value{...} mot faktiskt data från YAML let rec resolveMeta = function @@ -146,11 +141,11 @@ let processDocument (source: string) = | Paragraph children -> Paragraph (children |> List.map resolveMeta - |> List.map (Execution.transform myPrelude evaluator)) + |> List.map (Execution.transform metadata myPrelude evaluator)) | Section(l, a, children) -> Section(l, a, children |> List.map resolveMeta - |> List.map (Execution.transform myPrelude evaluator)) + |> List.map (Execution.transform metadata myPrelude evaluator)) ) // Steg 3: Be printern skriva ut trädet till HTML From 6b952bd6fd5950170ab52749063bd6748ae9bcba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?linus=20bj=C3=B6rnstam?= Date: Sat, 28 Mar 2026 14:20:34 +0100 Subject: [PATCH 2/2] playing with the interface --- src/FibLib/FibLib.fsproj | 2 +- src/FibLib/Library.fs | 119 +++++++++++++++++++++++++++++++-------- src/FibLib/Pandoc.fs | 38 +++++++++++++ src/FibLib/strip-p.lua | 5 ++ 4 files changed, 141 insertions(+), 23 deletions(-) create mode 100644 src/FibLib/Pandoc.fs create mode 100644 src/FibLib/strip-p.lua diff --git a/src/FibLib/FibLib.fsproj b/src/FibLib/FibLib.fsproj index ee25264..d424556 100644 --- a/src/FibLib/FibLib.fsproj +++ b/src/FibLib/FibLib.fsproj @@ -6,13 +6,13 @@ + - diff --git a/src/FibLib/Library.fs b/src/FibLib/Library.fs index a7af3c6..fd5a9ef 100644 --- a/src/FibLib/Library.fs +++ b/src/FibLib/Library.fs @@ -6,7 +6,43 @@ open YamlDotNet.Serialization open System.Collections.Generic +// ========================================== +// 1. AST & Utils +// ========================================== +module Ast = + type InlineNode = + | Text of string + | RawHtml of string + | Expr of code: string * result: string option + | Element of tag: string * args: (string * string) list * children: InlineNode list + + type BlockNode = + | Section of level: int * args: (string * string) list * children: InlineNode list + | Paragraph of children: InlineNode list + + type Document = BlockNode list + + type TagRenderer = Map -> (string * string) list -> InlineNode list -> InlineNode + + let rec stringifyNodes (nodes: InlineNode list) = + let tupleToString (t: string * string) = sprintf "%s=\"%s\"" (fst t) (snd t) + nodes + |> List.map (function + | Text t -> t + | RawHtml h -> h + | Element(tag, args, children) -> + // Omvandla inre taggar till HTML + let attrs = if args.IsEmpty then "" else " " + String.concat " " (List.map tupleToString args) + sprintf "<%s%s>%s" tag attrs (stringifyNodes children) tag + | Expr(_, Some res) -> res + | Expr(code, None) -> sprintf "@(%s)" code // Fallback om den inte evaluerats + ) + |> String.concat "" + + + module Utils = + open Ast let smartDedent (input: string) = let lines = input.Replace("\r\n", "\n").Split '\n' |> List.ofArray @@ -30,26 +66,30 @@ module Utils = // 3. Slå ihop och städa bort överflödiga radbrytningar i början/slutet (String.concat "\n" dedented).Trim('\n', '\r') - -// ========================================== -// 1. AST & Utils -// ========================================== -module Ast = - type InlineNode = - | Text of string - | RawHtml of string - | Expr of code: string * result: string option - | MetaRef of string - | Element of tag: string * args: string list * children: InlineNode list + let positional f: TagRenderer = + fun meta (args: (string*string) list) children -> f meta (List.map snd args) children - type BlockNode = - | Section of level: int * args: string list * children: InlineNode list - | Paragraph of children: InlineNode list + 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 - type Document = BlockNode list + // 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 - type TagRenderer = Map -> string list -> InlineNode list -> InlineNode + 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 + + let nameToElement (n:string) : TagRenderer = + fun meta args children -> Element(n, args, children) + @@ -72,7 +112,17 @@ module Parser = let isSection (name: string) = name.EndsWith("section") let pNewline = newline - let pArg = spaces >>. manyChars (noneOf ",]") .>> spaces + 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 ",")) // --- 1. Måsvinge-parser (för @kommandon) --- @@ -83,9 +133,17 @@ module Parser = pchar '{' >>. pRawBodyRef.Value .>> pchar '}' |>> sprintf "{%s}" ]) |>> String.concat "" - let pBody: Parser = + let pBody = between (pstring "{") (pstring "}") pRawBodyRef.Value >>= fun raw -> - match run (many pInline .>> eof) (Utils.smartDedent raw) with + // 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 | Success(n, _, _) -> preturn n | Failure(m, _, _) -> fail m @@ -187,7 +245,6 @@ module Execution = let rec transform (metadata: Map) (prelude: Map) (eval: IEvaluator) = function | Element(n, a, c) when prelude.ContainsKey n -> - // Skicka in metadata som första argument till din funktion prelude.[n] metadata a (c |> List.map (transform metadata prelude eval)) | Element(n, a, c) -> Element(n, a, c |> List.map (transform metadata prelude eval)) @@ -196,17 +253,35 @@ module Execution = module HtmlPrinter = open Ast - + let voidElements = [ "area"; "base"; "br"; "col"; "command"; "embed"; "hr"; "img"; "input"; "keygen"; "link"; "meta"; "param"; "source"; "track"; "wbr"] + let renderAttributes args = + args + |> List.map (fun m -> sprintf "%s=\"%s\"" (fst m) (snd m)) + |> String.concat " " let rec renderInline = function | Text t -> WebUtility.HtmlEncode t | RawHtml h -> h - | Element(t, a, c) -> sprintf "<%s>%s" t (c |> List.map renderInline |> String.concat "") t + | Element(t,a,c) when List.contains t voidElements -> sprintf "<%s %s />" t (renderAttributes a) + | Element(t, a, c) -> sprintf "<%s %s>%s" t (renderAttributes a) (c |> List.map renderInline |> String.concat "") t | _ -> "" let render (header, blocks) = blocks |> List.map (function + | Paragraph [RawHtml html] -> + html + + // 2. (Frivillig) Mer robust guard om parsern råkar lämna kvar + // blanksteg (Text " ") runt ditt @md-block i samma paragraf + | Paragraph nodes when nodes |> List.forall (function + | RawHtml _ -> true + | Text t when System.String.IsNullOrWhiteSpace(t) -> true + | _ -> false) -> + + nodes + |> List.choose (function RawHtml h -> Some h | _ -> None) + |> String.concat "\n" | Paragraph c -> sprintf "

%s

" (c |> List.map renderInline |> String.concat "") | Section(l, _, c) -> sprintf "%s" l (c |> List.map renderInline |> String.concat "") l) |> String.concat "\n" diff --git a/src/FibLib/Pandoc.fs b/src/FibLib/Pandoc.fs new file mode 100644 index 0000000..6b510e4 --- /dev/null +++ b/src/FibLib/Pandoc.fs @@ -0,0 +1,38 @@ +namespace Fibble.FibLib + +module Pandoc = + open System.Diagnostics + + let toHtml (from: string) (markdownText: string) = + let startInfo = ProcessStartInfo( + FileName = "pandoc", + Arguments = $"-f {from} -t html5 --lua-filter strip-p.lua", + RedirectStandardInput = true, + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true) + + try + use proc = Process.Start startInfo + // Skriv markdown till Pandoc + use stdin = proc.StandardInput + stdin.Write markdownText + stdin.Close() // Måste stängas så Pandoc vet att texten är slut + // Läs ut resultatet + let htmlOutput = proc.StandardOutput.ReadToEnd() + let errorOutput = proc.StandardError.ReadToEnd() + + proc.WaitForExit() + + if proc.ExitCode = 0 then + htmlOutput.Trim() + else + sprintf "\n
%s
and
%s
" errorOutput markdownText + + with ex -> + // Fångar upp om Pandoc inte är installerat eller inte finns i PATH + sprintf "\n
%s
and
%s
" ex.Message markdownText + + let mdToHtml markdownText = + toHtml "markdown" markdownText diff --git a/src/FibLib/strip-p.lua b/src/FibLib/strip-p.lua new file mode 100644 index 0000000..3d7d3d7 --- /dev/null +++ b/src/FibLib/strip-p.lua @@ -0,0 +1,5 @@ +function Pandoc(doc) + if #doc.blocks > 0 and doc.blocks[1].t == "Para" then + return pandoc.Pandoc(doc.blocks[1].content, doc.meta) + end +end