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] 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