it is now better than ever
This commit is contained in:
parent
b6b2958592
commit
78598d71e7
4 changed files with 543 additions and 152 deletions
|
|
@ -11,6 +11,7 @@
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="FParsec" Version="1.1.1" />
|
<PackageReference Include="FParsec" Version="1.1.1" />
|
||||||
|
<PackageReference Include="FSharp.Compiler.Service" Version="43.12.201" />
|
||||||
<PackageReference Include="System.Text.Json" Version="10.0.5" />
|
<PackageReference Include="System.Text.Json" Version="10.0.5" />
|
||||||
<PackageReference Include="YamlDotNet" Version="16.3.0" />
|
<PackageReference Include="YamlDotNet" Version="16.3.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,62 @@
|
||||||
namespace Fibble.FibLib
|
namespace Fibble.FibLib
|
||||||
|
|
||||||
open System.Net
|
open System.Net
|
||||||
open FParsec
|
open FParsec
|
||||||
open YamlDotNet.Serialization
|
open YamlDotNet.Serialization
|
||||||
open System.Collections.Generic
|
open System.Collections.Generic
|
||||||
|
|
||||||
|
|
||||||
|
module Utils =
|
||||||
|
let smartDedent (input: string) =
|
||||||
|
let lines = input.Replace("\r\n", "\n").Split('\n') |> List.ofArray
|
||||||
|
|
||||||
|
// 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
|
||||||
|
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')
|
||||||
|
|
||||||
|
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
|
||||||
|
// ==========================================
|
||||||
module Ast =
|
module Ast =
|
||||||
type InlineNode =
|
type InlineNode =
|
||||||
| Text of string
|
| Text of string
|
||||||
|
|
@ -18,9 +71,31 @@ module Ast =
|
||||||
|
|
||||||
type Document = BlockNode list
|
type Document = BlockNode list
|
||||||
|
|
||||||
|
type TagRenderer = 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 (FParsec)
|
// 2. Parser
|
||||||
// ==========================================
|
// ==========================================
|
||||||
|
|
||||||
module Parser =
|
module Parser =
|
||||||
open Ast
|
open Ast
|
||||||
|
|
||||||
|
|
@ -34,14 +109,50 @@ module Parser =
|
||||||
|
|
||||||
let isSection (name: string) = name.EndsWith("section")
|
let isSection (name: string) = name.EndsWith("section")
|
||||||
|
|
||||||
// --- Nya parsers för argument och body ---
|
let pNewline = newline
|
||||||
let pArg = spaces >>. manyChars (noneOf ",]") .>> spaces
|
let pArg = spaces >>. manyChars (noneOf ",]") .>> spaces
|
||||||
let pArgs = between (pstring "[") (pstring "]") (sepBy pArg (pstring ","))
|
let pArgs = between (pstring "[") (pstring "]") (sepBy pArg (pstring ","))
|
||||||
let pBody = between (pstring "{") (pstring "}") (many pInline)
|
|
||||||
|
|
||||||
// --- Inline Parsers ---
|
// --- 1. Måsvinge-parser (för @kommandon) ---
|
||||||
let pExpr = attempt (pstring "@(") >>. manyChars (noneOf ")") .>> pstring ")" |>> fun c -> Expr(c, None)
|
let pRawBody, pRawBodyRef = createParserForwardedToRef<string, unit>()
|
||||||
let pMetaRef = attempt (pstring "@value{") >>. many1Chars (noneOf "}") .>> pstring "}" |>> MetaRef
|
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
|
||||||
|
|
||||||
|
// --- 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)
|
||||||
|
|
||||||
let pInlineCommand =
|
let pInlineCommand =
|
||||||
attempt (
|
attempt (
|
||||||
|
|
@ -51,16 +162,17 @@ module Parser =
|
||||||
)
|
)
|
||||||
.>>. opt pArgs
|
.>>. opt pArgs
|
||||||
.>>. opt pBody
|
.>>. opt pBody
|
||||||
|>> fun ((name, argsOpt), bodyOpt) ->
|
|>> fun ((n, a), b) -> Element(n, defaultArg a [], defaultArg b [])
|
||||||
let args = defaultArg argsOpt []
|
|
||||||
let body = defaultArg bodyOpt []
|
|
||||||
Element(name, args, body)
|
|
||||||
|
|
||||||
let pText = many1Chars (noneOf "@{}\n\r") |>> Text
|
// MÅSTE tilldelas efter att alla pExpr, pInlineCommand etc. är definierade
|
||||||
let pNewline = newline
|
pInlineRef.Value <- choice [
|
||||||
let pSingleNewline = attempt (pNewline .>> notFollowedBy pNewline) |>> fun _ -> Text "\n"
|
pMultilineCode
|
||||||
|
pExpr
|
||||||
pInlineRef.Value <- choice [ pExpr; pMetaRef; pInlineCommand; pText; pSingleNewline ]
|
pInlineCommand
|
||||||
|
many1Chars (noneOf "@{}\n\r") |>> Text
|
||||||
|
attempt (pNewline .>> notFollowedBy pNewline) |>> fun _ -> Text "\n"
|
||||||
|
pchar '@' |>> fun _ -> Text "@"
|
||||||
|
]
|
||||||
|
|
||||||
// --- Block Parsers ---
|
// --- Block Parsers ---
|
||||||
let pSectionBlock =
|
let pSectionBlock =
|
||||||
|
|
@ -72,121 +184,292 @@ module Parser =
|
||||||
.>>. opt pArgs
|
.>>. opt pArgs
|
||||||
.>>. opt pBody
|
.>>. opt pBody
|
||||||
|>> fun ((name, argsOpt), bodyOpt) ->
|
|>> fun ((name, argsOpt), bodyOpt) ->
|
||||||
let args = defaultArg argsOpt []
|
Section(getSectionLevel name, defaultArg argsOpt [], defaultArg bodyOpt [])
|
||||||
let body = defaultArg bodyOpt []
|
|
||||||
Section(getSectionLevel name, args, body)
|
|
||||||
|
|
||||||
let pParagraphBlock = many1 pInline |>> Paragraph
|
let pParagraphBlock = many1 pInline |>> Paragraph
|
||||||
|
|
||||||
let pBlock = choice [ pSectionBlock; pParagraphBlock ]
|
let pBlock = choice [ pSectionBlock; pParagraphBlock ]
|
||||||
|
|
||||||
// --- YAML Header Parser ---
|
// --- Dokument Parser ---
|
||||||
let pYamlHeader =
|
let pDocument =
|
||||||
// Letar efter radbrytning, följt av ---, följt av radbrytning (eller filens slut)
|
spaces
|
||||||
let endMarker = attempt (newline >>. pstring "---" >>. (skipNewline <|> eof))
|
>>. opt (
|
||||||
// Matchar --- i början, läser allt som text fram till endMarker
|
pstring "---"
|
||||||
pstring "---" >>. newline >>. manyCharsTill anyChar endMarker
|
>>. manyCharsTill anyChar (attempt (pNewline >>. pstring "---"))
|
||||||
|>> fun yamlStr ->
|
|>> fun yamlStr ->
|
||||||
let deserializer = DeserializerBuilder().Build()
|
let deserializer = DeserializerBuilder().Build()
|
||||||
try
|
|
||||||
let dict = deserializer.Deserialize<Dictionary<string, string>>(yamlStr)
|
let dict = deserializer.Deserialize<Dictionary<string, string>>(yamlStr)
|
||||||
if isNull dict then Map.empty
|
if isNull dict then Map.empty
|
||||||
else dict |> Seq.map (fun kvp -> kvp.Key, kvp.Value) |> Map.ofSeq
|
else dict |> Seq.map (fun kvp -> kvp.Key, kvp.Value) |> Map.ofSeq
|
||||||
with _ -> Map.empty
|
)
|
||||||
|
.>> spaces
|
||||||
// --- Dokument Parser ---
|
.>>. sepEndBy pBlock (many1 pNewline)
|
||||||
let pDocument =
|
.>> eof
|
||||||
spaces >>. opt pYamlHeader .>> spaces .>>. sepEndBy pBlock (many1 pNewline)
|
|
||||||
|>> fun (headerOpt, blocks) ->
|
|>> fun (headerOpt, blocks) ->
|
||||||
(defaultArg headerOpt Map.empty, blocks)
|
(defaultArg headerOpt Map.empty, blocks)
|
||||||
|
|
||||||
let parse (input: string) : Map<string, string> * Document =
|
let parse i =
|
||||||
match run pDocument input with
|
match run pDocument i with
|
||||||
| Success(result, _, _) -> result
|
| Success(r, _, _) -> r
|
||||||
| Failure(errorMsg, _, _) -> failwithf "Kunde inte parsa dokumentet:\n%s" errorMsg
|
| Failure(e, _, _) -> failwith e
|
||||||
|
|
||||||
|
|
||||||
|
module Parser2 =
|
||||||
|
open Ast
|
||||||
|
|
||||||
|
let pInline, pInlineRef = createParserForwardedToRef<InlineNode, unit>()
|
||||||
|
|
||||||
|
// --- 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<string, unit>()
|
||||||
|
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<string, unit>()
|
||||||
|
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<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)
|
||||||
|
|>> fun (headerOpt, blocks) ->
|
||||||
|
(defaultArg headerOpt Map.empty, blocks)
|
||||||
|
|
||||||
|
let parse i =
|
||||||
|
match run pDocument i with
|
||||||
|
| Success(r, _, _) -> r
|
||||||
|
| Failure(e, _, _) -> failwith e
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// 3. Evaluator
|
// 3. Execution & Printer
|
||||||
// ==========================================
|
// ==========================================
|
||||||
type IEvaluator =
|
type IEvaluator =
|
||||||
abstract member Evaluate: code:string -> string
|
abstract member Evaluate: string -> string
|
||||||
|
|
||||||
module Execution =
|
module Execution =
|
||||||
open Ast
|
open Ast
|
||||||
|
|
||||||
let rec evaluateInline (metadata: Map<string, string>) (evaluator: IEvaluator) (node: InlineNode) =
|
let rec transform (prelude: Map<string, TagRenderer>) (eval: IEvaluator) =
|
||||||
match node with
|
function
|
||||||
| Expr (code, _) ->
|
| Element(n, a, c) when prelude.ContainsKey n -> prelude.[n] a (c |> List.map (transform prelude eval))
|
||||||
let result = evaluator.Evaluate(code)
|
| Element(n, a, c) -> Element(n, a, c |> List.map (transform prelude eval))
|
||||||
RawHtml result
|
| Expr(c, _) -> RawHtml(eval.Evaluate c)
|
||||||
| Text t -> Text t
|
| n -> n
|
||||||
| RawHtml h -> RawHtml h
|
|
||||||
| MetaRef key ->
|
|
||||||
match metadata.TryFind key with
|
|
||||||
| Some value -> Text value
|
|
||||||
| None -> Text (sprintf "[Saknad meta: %s]" key)
|
|
||||||
| Element (tag, args, children) ->
|
|
||||||
Element (tag, args, children |> List.map (evaluateInline metadata evaluator))
|
|
||||||
| Text t -> Text t
|
|
||||||
|
|
||||||
let evaluateBlock (metadata: Map<string, string>) (evaluator: IEvaluator) (node: BlockNode) =
|
module HtmlPrinter =
|
||||||
match node with
|
|
||||||
| Section (level, args, children) -> Section (level, args, children |> List.map (evaluateInline metadata evaluator))
|
|
||||||
| Paragraph children -> Paragraph (children |> List.map (evaluateInline metadata evaluator))
|
|
||||||
|
|
||||||
let evaluateDocument (metadata: Map<string, string>) (evaluator: IEvaluator) (doc: Document) =
|
|
||||||
doc |> List.map (evaluateBlock metadata evaluator)
|
|
||||||
|
|
||||||
// ==========================================
|
|
||||||
// 4. HTML Converter
|
|
||||||
// ==========================================
|
|
||||||
type TagRenderer = string list -> string -> string
|
|
||||||
|
|
||||||
type IConverter =
|
|
||||||
abstract member Convert: doc:Ast.Document * metadata:Map<string, string> -> string
|
|
||||||
|
|
||||||
module Converters =
|
|
||||||
open Ast
|
open Ast
|
||||||
|
|
||||||
type HtmlTemplateConverter(template: string, prelude: Map<string, TagRenderer>) =
|
let rec renderInline =
|
||||||
|
function
|
||||||
let rec renderInline = function
|
| Text t -> WebUtility.HtmlEncode t
|
||||||
| Text t -> WebUtility.HtmlEncode(t)
|
|
||||||
| RawHtml h -> h
|
| RawHtml h -> h
|
||||||
| Expr (_, Some res) -> WebUtility.HtmlEncode(res)
|
| Element(t, a, c) -> sprintf "<%s>%s</%s>" t (c |> List.map renderInline |> String.concat "") t
|
||||||
| Expr (_, None) -> failwith "Oexekverat uttryck"
|
| _ -> ""
|
||||||
| MetaRef _ -> failwith "Ogiltig nod vid konvertering"
|
|
||||||
| Element (tag, args, children) ->
|
|
||||||
let renderedChildren = renderInlines children
|
|
||||||
match prelude.TryFind tag with
|
|
||||||
| Some customFunc -> customFunc args renderedChildren
|
|
||||||
| None -> sprintf "<%s>%s</%s>" tag renderedChildren tag // Fallback ignorerar args
|
|
||||||
|
|
||||||
and renderInlines inlines =
|
let render (header, blocks) =
|
||||||
inlines |> List.map renderInline |> String.concat ""
|
blocks
|
||||||
|
|> List.map (function
|
||||||
|
| Paragraph c -> sprintf "<p>%s</p>" (c |> List.map renderInline |> String.concat "")
|
||||||
|
| Section(l, _, c) -> sprintf "<h%d>%s</h%d>" l (c |> List.map renderInline |> String.concat "") l)
|
||||||
|
|> String.concat "\n"
|
||||||
|
|
||||||
let renderBlock = function
|
|
||||||
| Section (level, args, children) ->
|
|
||||||
let tag = sprintf "h%d" level
|
|
||||||
let idAttr = if args.Length > 0 then sprintf " id=\"%s\"" (args.[0].Trim('"')) else ""
|
|
||||||
sprintf "<%s%s>%s</%s>" tag idAttr (renderInlines children) tag
|
|
||||||
| Paragraph children ->
|
|
||||||
sprintf "<p>%s</p>" (renderInlines children)
|
|
||||||
|
|
||||||
interface IConverter with
|
module Evaluators =
|
||||||
member _.Convert(doc, metadata) =
|
open System.IO
|
||||||
let bodyHtml = doc |> List.map renderBlock |> String.concat "\n\n"
|
open System.Text
|
||||||
let mutable finalHtml = template.Replace("{{body}}", bodyHtml)
|
open FSharp.Compiler.Interactive.Shell
|
||||||
for kvp in metadata do
|
|
||||||
finalHtml <- finalHtml.Replace(sprintf "{{%s}}" kvp.Key, WebUtility.HtmlEncode(kvp.Value))
|
|
||||||
finalHtml
|
|
||||||
|
|
||||||
// ==========================================
|
type FsiEvaluator() =
|
||||||
// 5. Pipeline & Test
|
let sbOut = new StringBuilder()
|
||||||
// ==========================================
|
let sbErr = new StringBuilder()
|
||||||
module FibLib=
|
let inStream = new StringReader("")
|
||||||
let processDocument (source: string) (template: string) (evaluator: IEvaluator) (converter: IConverter) =
|
let outStream = new StringWriter(sbOut)
|
||||||
let (metadata, rawAst) = Parser.parse source
|
let errStream = new StringWriter(sbErr)
|
||||||
let evaluatedAst = Execution.evaluateDocument metadata evaluator rawAst
|
|
||||||
converter.Convert(evaluatedAst, metadata)
|
|
||||||
|
|
||||||
|
// 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
|
||||||
|
|
||||||
|
// 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 """
|
||||||
|
// <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 ->
|
||||||
|
// // Fångar upp om själva anropet till session.EvalInteractionNonThrowing kraschar helt
|
||||||
|
// sprintf """<div style="color: red; border: 1px solid red; padding: 10px;">
|
||||||
|
// <strong>Kritiskt FSI-systemfel:</strong> %s
|
||||||
|
// </div>""" ex.Message
|
||||||
|
|
|
||||||
|
|
@ -13,4 +13,9 @@
|
||||||
<ProjectReference Include="..\FibLib\FibLib.fsproj" />
|
<ProjectReference Include="..\FibLib\FibLib.fsproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="FParsec" Version="1.1.1" />
|
||||||
|
<PackageReference Include="YamlDotNet" Version="16.3.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
||||||
|
|
@ -1,66 +1,168 @@
|
||||||
open Fibble.FibLib
|
open System.Net
|
||||||
|
open Fibble.FibLib
|
||||||
|
open Fibble.FibLib.Ast // Ger oss tillgång till Element, Text, RawHtml etc.
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// 1. Prelude (Dina egna taggar)
|
||||||
|
// ==========================================
|
||||||
|
|
||||||
|
// makeElem returnerar nu en strukturerad Element-nod istället för en sträng
|
||||||
let makeElem (name : string) =
|
let makeElem (name : string) =
|
||||||
("name", fun _ text -> sprintf "<%s>%s</%s>" name text name)
|
(name, fun args children -> Element(name, args, children))
|
||||||
|
|
||||||
let makeElems lst =
|
let makeElems lst =
|
||||||
List.map makeElem lst
|
List.map makeElem lst
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
let defaultPrelude : Map<string, TagRenderer> =
|
let defaultPrelude : Map<string, TagRenderer> =
|
||||||
[ "b"
|
[ "b"; "em"; "i"; "strong" ]
|
||||||
"em"
|
|
||||||
"i"
|
|
||||||
"strong" ]
|
|
||||||
|> makeElems
|
|> makeElems
|
||||||
|> Map.ofList
|
|> Map.ofList
|
||||||
|
|
||||||
let myPrelude : Map<string, TagRenderer> =
|
let myPrelude : Map<string, TagRenderer> =
|
||||||
Map.ofList [
|
Map.ofList [
|
||||||
"quotient", fun args _ ->
|
"quotient", fun args _ ->
|
||||||
if args.Length >= 2 then sprintf "%d" (int args.[0] / int args.[1])
|
if args.Length >= 2 then
|
||||||
else "[Fel: quotient kräver två argument]"
|
Text (sprintf "%d" (int args.[0] / int args.[1]))
|
||||||
|
else
|
||||||
|
Text "[Fel: quotient kräver två argument]"
|
||||||
|
|
||||||
"bold", fun _ body -> sprintf "<b>%s</b>" body
|
"bold", fun args children ->
|
||||||
|
Element("b", args, children)
|
||||||
|
|
||||||
"link", fun args body ->
|
"link", fun args children ->
|
||||||
let url = if args.Length > 0 then args.[0].Trim('"') else "#"
|
let url = if args.Length > 0 then args.[0].Trim('"') else "#"
|
||||||
sprintf """<a href="%s">%s</a>""" url body
|
// 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/>"
|
||||||
]
|
]
|
||||||
|
|
||||||
let template = """<!DOCTYPE html>
|
// ==========================================
|
||||||
<html>
|
// 2. Mall och Evaluator
|
||||||
<head><title>{{title}}</title></head>
|
// ==========================================
|
||||||
|
let pageTemplate = """ <!DOCTYPE html>
|
||||||
|
<htm>
|
||||||
|
<head>
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||||
|
<meta http-equiv="Content-Style-Type" content="text/css" />
|
||||||
|
<meta name="viewport" content="initial-scale=1,maximum-scale=1" />
|
||||||
|
<link rel="stylesheet" type="text/css" href="/_resources/min.css">
|
||||||
|
<title>{{title}}</title>
|
||||||
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<h1>{{title}}</h1>
|
|
||||||
|
<nav class="nav" id="nav">
|
||||||
|
<div class="container">
|
||||||
|
<a class="pagename current" href="/"><img src="/_resources/icons/go-home.svg.png"/>Home</a>
|
||||||
|
<a href="."><img src="/_resources/icons/go-up.svg.png"/>Up</a>
|
||||||
|
<a href="/om/"><img src="/_resources/icons/help-contents.svg.png"/>About and Credits</a>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
<a href="#nav" class="btn-nav"></a>
|
||||||
|
<a href="#close" class="btn btn-sm btn-close">×</a>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
|
||||||
|
<h1 class="title">{{title}}</h1>
|
||||||
|
<div class="dateandauthor">
|
||||||
|
<span class="author">by {{author}}</span>
|
||||||
|
<span class="date">{{date}}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr/>
|
||||||
{{body}}
|
{{body}}
|
||||||
|
|
||||||
|
<!-- FOOTER -->
|
||||||
|
<hr/>
|
||||||
|
<div class="footer">
|
||||||
|
All images (except icons) are, unless otherwise specified, in the public domain. For the rest of the content, the following apply: <br/>
|
||||||
|
|
||||||
|
<div class="cclicence">
|
||||||
|
<div class="licence-image">
|
||||||
|
<a rel="license" href="http://creativecommons.org/licenses/by-sa/4.0/">
|
||||||
|
<img alt="Creative Commons License" style="border-width:0" src="https://i.creativecommons.org/l/by-sa/4.0/88x31.png" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="licence-text">
|
||||||
|
This work by <a xmlns:cc="http://creativecommons.org/ns#" href="mailto:berglund_linus@fastmail.se" property="cc:attributionName" rel="cc:attributionURL">Linus Björstam</a> is licensed under a <a rel="license" href="http://creativecommons.org/licenses/by-sa/4.0/">Creative Commons Attribution-ShareAlike 4.0 International License</a>.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div><!-- end class container -->
|
||||||
</body>
|
</body>
|
||||||
</html>"""
|
</html>
|
||||||
|
>"""
|
||||||
type MockEvaluator() =
|
|
||||||
interface IEvaluator with
|
|
||||||
member _.Evaluate(code) = if code.Trim() = "2 + 2" then "4" else "Okänd"
|
|
||||||
|
|
||||||
let sourceCode = """---
|
|
||||||
title: Min Rapport
|
|
||||||
author: Ada
|
|
||||||
|
|
||||||
|
let sourceCode = "---
|
||||||
|
author: bajs
|
||||||
|
date: yesterday
|
||||||
|
title: FSI Test
|
||||||
---
|
---
|
||||||
|
|
||||||
@section["intro"]{Inledning}
|
@section{Definitioner}
|
||||||
Detta dokument av @value{author} visar hur positionella argument fungerar.
|
Vi definierar en funktion och en variabel.
|
||||||
|
@\"\"\"
|
||||||
|
let globalCounter = ref 0
|
||||||
|
|
||||||
Resultatet av quotient är @quotient[20, 6].
|
let increment () =
|
||||||
En vanlig F#-beräkning: @(2 + 2)
|
globalCounter.Value <- globalCounter.Value + 1
|
||||||
|
sprintf \"Räknaren är nu %d\" globalCounter.Value
|
||||||
|
\"\"\"
|
||||||
|
|
||||||
@bold{Fet text} och en @link["https://fsharp.org"]{länk till F#}.
|
@(1)
|
||||||
En tagg helt utan argument eller kropp: @br"""
|
|
||||||
|
|
||||||
let converter = Converters.HtmlTemplateConverter(template, myPrelude)
|
@section{Användning}
|
||||||
let evaluator = MockEvaluator()
|
Första anropet: @(increment())
|
||||||
|
Andra anropet: @(increment())
|
||||||
|
|
||||||
let output = FibLib.processDocument sourceCode template evaluator converter
|
Test av utskrift:
|
||||||
|
"
|
||||||
|
// ==========================================
|
||||||
|
// 3. Huvudpipeline
|
||||||
|
// ==========================================
|
||||||
|
let processDocument (source: string) =
|
||||||
|
let evaluator = Evaluators.FsiEvaluator()
|
||||||
|
|
||||||
|
// Steg 1: Parsa koden
|
||||||
|
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
|
||||||
|
| MetaRef key ->
|
||||||
|
match metadata.TryFind key with
|
||||||
|
| Some v -> Text v
|
||||||
|
| None -> Text (sprintf "[Saknad meta: %s]" key)
|
||||||
|
| Element(tag, args, children) ->
|
||||||
|
Element(tag, args, children |> List.map resolveMeta)
|
||||||
|
| other -> other
|
||||||
|
|
||||||
|
// Steg 2: Transformera och exekvera trädet
|
||||||
|
let evaluatedBlocks =
|
||||||
|
rawBlocks |> List.map (function
|
||||||
|
| Paragraph children ->
|
||||||
|
Paragraph (children
|
||||||
|
|> List.map resolveMeta
|
||||||
|
|> List.map (Execution.transform myPrelude evaluator))
|
||||||
|
| Section(l, a, children) ->
|
||||||
|
Section(l, a, children
|
||||||
|
|> List.map resolveMeta
|
||||||
|
|> List.map (Execution.transform myPrelude evaluator))
|
||||||
|
)
|
||||||
|
|
||||||
|
// Steg 3: Be printern skriva ut trädet till HTML
|
||||||
|
let bodyHtml = HtmlPrinter.render (metadata, evaluatedBlocks)
|
||||||
|
|
||||||
|
// Steg 4: Fyll i din HTML-mall
|
||||||
|
let mutable finalHtml = pageTemplate.Replace("{{body}}", bodyHtml)
|
||||||
|
for kvp in metadata do
|
||||||
|
finalHtml <- finalHtml.Replace(sprintf "{{%s}}" kvp.Key, WebUtility.HtmlEncode(kvp.Value))
|
||||||
|
|
||||||
|
finalHtml
|
||||||
|
|
||||||
|
// Kör programmet
|
||||||
|
let output = processDocument sourceCode
|
||||||
printfn "%s" output
|
printfn "%s" output
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue