Compare commits

...

3 commits

Author SHA1 Message Date
c3f2f98dc8 TagRenderers can output Blocks, we now slurp sections
The big one is probably that tagrenderers can output blocks
such as tables. We also slurp sections, so that subsections
become a part of the above sections children.

The FSI evaluator adds about 1000% of runtime for testing, so
I added a NullEvaluator.

I also added default constructors for many of the base elements.

The parsers are not yet done, but you can do very rudimentary tables.

The AST has gotten a large update, and it is pretty much complete.
2026-04-02 10:06:59 +02:00
f1fefece72 merge remote 2026-04-01 08:57:37 +02:00
6e26f37759 Changed name of rawhtml 2026-04-01 08:52:00 +02:00
11 changed files with 875 additions and 404 deletions

6
Fibble.slnx Normal file
View file

@ -0,0 +1,6 @@
<Solution>
<Folder Name="/src/">
<Project Path="src/Bif/Bif.fsproj" />
<Project Path="src/FibLib/FibLib.fsproj" />
</Folder>
</Solution>

View file

@ -7,6 +7,9 @@
<ItemGroup>
<Compile Include="Program.fs" />
<Content Include="document.fib" />
<Content Include="strip-p.lua" />
<Content Include="_page-template" />
</ItemGroup>
<ItemGroup>

View file

@ -1,46 +1,31 @@
open System.Net
open System.IO
open System.Runtime.CompilerServices
open FSharp.Compiler.Text
open System.IO
open Fibble.FibLib
open Fibble.FibLib.Ast // Ger oss tillgång till Element, Text, RawHtml etc.
open Fibble.FibLib.Pandoc
open Fibble.FibLib.Ast
open Fibble.FibLib.HtmlPrinter
open Fibble.FibLib.Utils
open Fibble.FibLib.ConstructionHelpers
// ==========================================
// 1. Prelude (Dina egna taggar)
// ==========================================
let myPrelude: Map<string, TagRenderer> =
Map [
"quotient", fun _ args _ _ ->
Map
[ "quotient",
fun _ args _ _ ->
match args with
| [one; two] ->
Text (sprintf "%d" (int one / int two))
| _ ->
Text "[Fel: quotient kräver två argument]"
"bold", fun _ _ _ children ->
Strong(children)
"kursiv", fun _ _ _ children -> Emph(children)
| [ one; two ] -> Inline(Text(sprintf "%d" (int one / int two)))
| _ -> Inline(Text "[Fel: quotient kräver två argument]")
"bold", fun _ _ _ children -> Inline(Strong(children))
"kursiv", fun _ _ _ children -> Inline(Emph(children))
"image", image
"value", value
"link", link
"list", fun _ _ _ c -> System.Console.WriteLine(c)
RawHtml "hej"
// @br har varken argument eller barn, vi returnerar bara HTML direkt
"list",
fun _ _ _ c ->
System.Console.WriteLine(c)
Inline(RawHtml "hej")
"br", linebreak
"table", fun _ _ _ children -> Text "hej"
"md", fun _ _ _ children -> RawHtml (mdToHtml (stringifyNodes children))
]
"table", table
"md", fun _ _ _ _ -> Inline(Text "hej") ]
// ==========================================
// 2. Mall och Evaluator
// ==========================================
//
module File =
let readFile path =
match Path.Exists(path) with
@ -50,31 +35,21 @@ module File =
let pageTemplate = File.readFile "_page-template"
let sourceCode = File.readFile "document.fib"
// ==========================================
// 3. Huvudpipeline
// ==========================================
let processDocument (source: string) =
let evaluator = Evaluators.FsiEvaluator()
let evaluator = Evaluators.NullEvaluator()
// Steg 1: Parsa koden
let metadata, rawBlocks = Parser.parse source
// Parsa koden
let metadata, raewBlocks = Parser.parse source
let rawBlocks = AstUtils.slurpSections raewBlocks
// Steg 2: Transformera och exekvera trädet
let evaluatedBlocks =
rawBlocks |> List.map (function
| Paragraph children ->
Paragraph (children
|> List.map (Execution.transform metadata myPrelude evaluator))
| Section(l, a, children) ->
Section(l, a, children
|> List.map (Execution.transform metadata myPrelude evaluator))
| _ -> failwith "haha"
CommandEvaluator.evaluateDocument metadata myPrelude evaluator rawBlocks
)
// Steg 3: Be printern skriva ut trädet till HTML
let bodyHtml = HtmlPrinter.render evaluatedBlocks
// Be printern skriva ut trädet till HTML
let bodyHtml = HtmlWriter.renderDocument evaluatedBlocks
// Steg 4: Fyll i din HTML-mall
let mutable finalHtml = pageTemplate.Replace("{{body}}", bodyHtml)

53
src/Bif/_page-template Normal file
View file

@ -0,0 +1,53 @@
<!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>
<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}}
<!-- 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>
</html>

View file

@ -6,44 +6,10 @@ title: FSI Test
@section{Definitioner}
Vi definierar en funktion och en variabel.
@"""
let globalCounter = ref 0
let increment () =
globalCounter.Value <- globalCounter.Value + 1
sprintf "Räknaren är nu %d" globalCounter.Value
"""
@(1)
@link[www.google.com]{hejsan hoppsan fallerallera}
Detta är en paragraf som har lite text i sig och lite @md{inbäddad *markdown* som kanske} funkar. Det är bra så.
@md{
babeuoastnuhaoesn
hej hopp
--- ----
abc defg
bde dddd
ueo eoau
ueo aoeu
@table{
rubrik 1 | rubrik 2
1 | 3
2 | 4
}
elleR?
@list{
Första saken
Andra saken med @bold{text}
Tredje saken
}
@section{Användning}
Första anropet: @(increment())
Andra anropet: @(increment())
@bold{detta är ibdenrerat.
detta också.
hej}
Test av utskrift: @value[date]

115
src/FibLib/AstUtils.fs Normal file
View file

@ -0,0 +1,115 @@
namespace Fibble.FibLib
module AstUtils =
open Ast
// Hjälpfunktion som samlar block rekursivt
let rec private gatherBlocks (currentLevel: int) (nodes: BlockNode list) : BlockNode list * BlockNode list =
match nodes with
| [] -> [], []
// Byt namn till 'existingBlocks' för tydlighet
| Section(attr, lvl, title, existingBlocks) :: tail when lvl > currentLevel ->
// 1. Samla in de Nya blocken som tillhör undersektionen från tail
let gatheredBlocks, restAfterSub = gatherBlocks lvl tail
// SÄKERHETSFIX: Slå ihop de gamla och de nya blocken!
let combinedBlocks = existingBlocks @ gatheredBlocks
let newSection = Section(attr, lvl, title, combinedBlocks)
// 2. Fortsätt samla in syskon till denna undersektion
let siblingBlocks, remaining = gatherBlocks currentLevel restAfterSub
newSection :: siblingBlocks, remaining
// Vi hittar en sektion samma eller "högre" nivå
| Section(_, lvl, _, _) :: _ when lvl <= currentLevel -> [], nodes
// Vanliga block (Paragraph, Table, etc.)
| block :: tail ->
let siblingBlocks, remaining = gatherBlocks currentLevel tail
block :: siblingBlocks, remaining
let slurpSections ast =
let nestedAst, _ = gatherBlocks 0 ast
nestedAst
let splitByNewlines (nodes: InlineNode list) =
// Folder-funktionen håller koll på: (Pågående rad, Lista med färdiga rader)
let folder (currentLine, completedLines) node =
match node with
| Text t ->
// Splitta texten. Vi tar bort \r för att stödja både Windows och Mac/Linux.
let lines = t.Replace("\r", "").Split('\n') |> Array.toList
match lines with
| [] -> (currentLine, completedLines) // Händer aldrig med Split
| [ single ] ->
// Ingen radbrytning hittades, fortsätt bygga nuvarande rad
(Text single :: currentLine, completedLines)
| first :: rest ->
// Vi hittade minst en radbrytning!
// 1. Avsluta den pågående raden med första delen av texten
let finishedFirstLine = List.rev (Text first :: currentLine)
// 2. Den sista biten text blir början nästa rad
let lastPart = rest |> List.last
// 3. Allt i mitten är egna fristående rader
let middleLines =
rest |> List.take (rest.Length - 1) |> List.map (fun s -> [ Text s ])
// Skicka tillbaka (Ny pågående rad, Uppdaterad lista med färdiga rader)
([ Text lastPart ], completedLines @ [ finishedFirstLine ] @ middleLines)
| otherNode ->
// Är det t.ex. ett Command lägger vi bara till det pågående rad
(otherNode :: currentLine, completedLines)
// Kör foldern över alla noder
let finalLine, allCompletedLines = List.fold folder ([], []) nodes
let allLinesRaw = allCompletedLines @ [ List.rev finalLine ]
// --- STÄDNING ---
// Ta bort tomma text-noder (skräp från splitten) och kasta helt tomma rader
allLinesRaw
|> List.map (
List.choose (function
| Text t when System.String.IsNullOrWhiteSpace(t) -> None
| validNode -> Some validNode)
)
|> List.filter (not << List.isEmpty)
let splitByPipe (nodes: InlineNode list) =
let folder (currentCell, completedCells) node =
match node with
| Text t ->
// Klyv rör-tecknet istället
let parts = t.Split('|') |> Array.toList
match parts with
| [] -> (currentCell, completedCells)
| [ single ] -> (Text single :: currentCell, completedCells)
| first :: rest ->
let finishedFirstCell = List.rev (Text first :: currentCell)
let lastPart = rest |> List.last
let middleCells =
rest |> List.take (rest.Length - 1) |> List.map (fun s -> [ Text s ])
([ Text lastPart ], completedCells @ [ finishedFirstCell ] @ middleCells)
| otherNode -> (otherNode :: currentCell, completedCells)
let finalCell, allCompletedCells = List.fold folder ([], []) nodes
let allCellsRaw = allCompletedCells @ [ List.rev finalCell ]
// Städa bort helt tomma text-noder som uppstår vid klyvningen
allCellsRaw
|> List.map (
List.choose (function
| Text t when System.String.IsNullOrWhiteSpace(t) -> None
| validNode -> Some validNode)
)

View file

@ -8,6 +8,7 @@
<ItemGroup>
<Compile Include="Pandoc.fs" />
<Compile Include="Library.fs" />
<Compile Include="AstUtils.fs" />
<Compile Include="constructorHelpers.fs" />
<Compile Include="HtmlPrinter.fs" />
</ItemGroup>

View file

@ -4,60 +4,151 @@ module Fibble.FibLib.HtmlPrinter
open System.Net
module HtmlPrinter =
module HtmlWriter =
open Ast
open System.Net
let renderAttributes (args: (string * string) list) =
args
|> List.map (fun k -> sprintf "%s=\"%s\"" (fst k) (snd k))
|> String.concat " "
let rec renderInline =
let renderAll l =
l
|> List.map renderInline
|> String.concat ""
function
| Text t -> WebUtility.HtmlEncode t
| Strong(t) -> sprintf "<strong>%s</strong>" (renderAll t)
| RawHtml h ->
h
| _ -> ""
let renderFigure a c l =
failwith "haha"
// --- HJÄLPARFUNKTIONER ---
let renderListItem item =
sprintf "<li>%s</li>" item
let escapeHtml (text: string) =
if System.String.IsNullOrEmpty(text) then
""
else
WebUtility.HtmlEncode(text)
let renderList kind attributes nodesList =
let content = nodesList
|> List.map renderListItem
|> String.concat "\n"
$"<{kind} {(renderAttributes attributes)}>{content}</{kind}>"
let rec render blocks =
let doubleRender blocksblock =
List.map render blocksblock
let renderNode = function
| Paragraph [RawHtml html] ->
html
| Paragraph nodes when nodes |> List.forall (function
| RawHtml _ -> true
| Text t when System.String.IsNullOrWhiteSpace(t) -> true
| _ -> false) ->
// Plockar ut ren text för attribut som alt-texter där vi inte får ha HTML-taggar
let rec extractText (nodes: InlineNode list) =
nodes
|> List.choose (function RawHtml h -> Some h | _ -> None)
|> String.concat "\n"
| 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
| CodeBlock(attributes, text) -> sprintf "<code>%s</code>" text
| Figure(attributes, caption, blocks) -> renderFigure attributes caption blocks
| ListBlock(l) -> match l with
| BulletList(attr, blocknodes) -> renderList "ul" attr.kvp (doubleRender blocknodes)
| Orderedlist(attr, start, blocknodes) -> renderList "ol" attr.kvp (doubleRender blocknodes)
| Plain nodes -> List.map renderInline nodes |> String.concat " "
blocks
|> List.map renderNode
|> String.concat "\n"
|> List.map (function
| Text t -> t
| RawHtml h -> h
| Emph c
| Underline c
| Strong c
| Strikeout c
| Superscript c
| Subscript c -> extractText c
| Link(_, (_, c)) -> extractText c
| Code(_, t) -> t
| _ -> "")
|> String.concat ""
// Bygger attributsträngen: id="X" class="Y Z" key="value"
let renderAttr (attr: Attr) =
let idStr =
if System.String.IsNullOrWhiteSpace(attr.id) then
""
else
sprintf " id=\"%s\"" attr.id
let classStr =
match attr.classes with
| [] -> ""
| classes -> sprintf " class=\"%s\"" (String.concat " " classes)
let kvpStr =
match attr.kvp with
| [] -> ""
| kvps ->
let pairs = kvps |> List.map (fun (k, v) -> sprintf "%s=\"%s\"" k v)
" " + (String.concat " " pairs)
idStr + classStr + kvpStr
// --- HUVUDRENDERARE ---
let rec renderDocument (blocks: BlockNode list) =
blocks |> List.map renderBlock |> String.concat ""
// Renderar Block-noder
and renderBlock =
function
| Paragraph children -> sprintf "<p>%s</p>\n" (renderInlines children)
| Plain inlines -> sprintf "%s\n" (renderInlines inlines)
| CodeBlock(attr, text) -> sprintf "<pre><code%s>%s</code></pre>\n" (renderAttr attr) (escapeHtml text)
| Figure(attr, caption, blocks) ->
let bodyHtml = blocks |> List.map renderBlock |> String.concat ""
let captionHtml = renderInlines caption
sprintf "<figure%s>\n%s<figcaption>%s</figcaption>\n</figure>\n" (renderAttr attr) bodyHtml captionHtml
| Section(attr, lvl, title, body) ->
// Begränsa h-nivåer till h1-h6 för giltig HTML
let hLvl = max 1 (min 6 lvl)
let titleHtml = renderInlines title
let bodyHtml = body |> List.map renderBlock |> String.concat ""
sprintf "<section%s>\n<h%d>%s</h%d>\n%s</section>\n" (renderAttr attr) hLvl titleHtml hLvl bodyHtml
| ListBlock kind ->
match kind with
| Orderedlist(attr, start, blocksList) ->
let startAttr = if start <> 1 then sprintf " start=\"%d\"" start else ""
let itemsHtml =
blocksList
|> List.map (fun b -> sprintf "<li>%s</li>\n" (b |> List.map renderBlock |> String.concat ""))
|> String.concat ""
sprintf "<ol%s%s>\n%s</ol>\n" (renderAttr attr) startAttr itemsHtml
| BulletList(attr, blocksList) ->
let itemsHtml =
blocksList
|> List.map (fun b -> sprintf "<li>%s</li>\n" (b |> List.map renderBlock |> String.concat ""))
|> String.concat ""
sprintf "<ul%s>\n%s</ul>\n" (renderAttr attr) itemsHtml
| Table(attr, rows) ->
let renderedRows = List.map renderRow rows |> String.concat " "
sprintf "<table%s>%s</table>" (renderAttr attr) renderedRows
// Renderar en lista av Inline-noder
and renderInlines inlines =
inlines |> List.map renderInline |> String.concat ""
// Renderar enskilda Inline-noder
and renderInline =
function
| Text t -> escapeHtml t
| RawHtml h -> h
// Formatering
| Emph c -> sprintf "<em>%s</em>" (renderInlines c)
| Strong c -> sprintf "<strong>%s</strong>" (renderInlines c)
| Underline c -> sprintf "<u>%s</u>" (renderInlines c)
| Strikeout c -> sprintf "<s>%s</s>" (renderInlines c)
| Superscript c -> sprintf "<sup>%s</sup>" (renderInlines c)
| Subscript c -> sprintf "<sub>%s</sub>" (renderInlines c)
// Länkar och bilder
| Link(attr, (url, content)) -> sprintf "<a href=\"%s\"%s>%s</a>" url (renderAttr attr) (renderInlines content)
| Image(attr, altText, url) ->
// Bild-alt måste vara ren text (får ej innehålla <em>, <strong> etc)
let cleanAlt = extractText altText |> escapeHtml
sprintf "<img src=\"%s\" alt=\"%s\"%s />" url cleanAlt (renderAttr attr)
| Code(attr, text) -> sprintf "<code%s>%s</code>" (renderAttr attr) (escapeHtml text)
| SoftBreak -> "\n"
| LineBreak -> "<br />"
| Expr(_, resultOpt) -> resultOpt |> Option.defaultValue ""
| Note _ ->
// Fotnoter renderas oftast som en ankarlänk här, och själva innehållet längst ner sidan.
// (Detta kan vi implementera senare när du bygger det globala state-systemet)
"<sup><a href=\"#footnote\">[ref]</a></sup>"
| Command(tag, _, _, _) ->
// Denna nod borde inte existera längre om Evaluatorn har gjort sitt jobb
failwithf "Kritiskt fel: Oexpanderat makro '@%s' nådde HTML-renderaren!" tag
and renderRow (Row cells) =
let renderedCell (Cell children) =
sprintf "<td>%s</td>" (renderInlines children)
List.map renderedCell cells |> String.concat "" |> sprintf "<tr>%s</tr>\n"

View file

@ -1,6 +1,7 @@
namespace Fibble.FibLib
open System.Net
open System
open FParsec
open YamlDotNet.Serialization
open System.Collections.Generic
@ -11,7 +12,10 @@ open System.Collections.Generic
// ==========================================
module Ast =
type Attr = { id: string; classes: string list; kvp: (string * string) list }
type Attr =
{ id: string
classes: string list
kvp: (string * string) list }
type InlineNode =
@ -31,62 +35,82 @@ module Ast =
| LineBreak
| Expr of code: string * result: string option
| Command of tag: string * args: string list * kwargs: Map<string, string> * children: InlineNode list
and BlockNode =
| BlockCommand of tag: string * args: string list * kwargs: Map<string, string> * children: InlineNode list
| CodeBlock of attributes: Attr * text: string
| Figure of attributes: Attr * caption: InlineNode list * blocks: BlockNode list
| ListBlock of ListKind
| Plain of InlineNode list
| Paragraph of children: InlineNode list
| Section of level: int * args: (string * string) list * children: InlineNode list
| Section of attributes: Attr * level: int * title: InlineNode list * body: BlockNode list
| Table of attrs: Attr * rows: TableRow list
and TableRow = Row of cells: TableCell list
and TableCell = Cell of 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
type Document = BlockNode list
type TagRenderer = Map<string,string>
-> string list
-> Map<string,string>
-> InlineNode list -> InlineNode
type TocEntry =
| TocGroup of title: string * id: string * children: TocEntry list
| TocEntity of title: string * id: string
type NextDocument =
{ document: BlockNode list
Toc: TocEntry list
Footnotes: BlockNode list list
Metadata: Map<string, string> }
type NodeResult =
| Inline of InlineNode
| Block of BlockNode
type TagRenderer = Map<string, string> -> string list -> Map<string, string> -> InlineNode list -> NodeResult
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
| _ -> failwith "haha"
)
|> String.concat ""
module Utils =
open Ast
let dedentNodes (nodes: InlineNode list) =
let fullText =
nodes |> List.choose (function Text t -> Some t | _ -> None) |> String.concat ""
nodes
|> List.choose (function
| Text t -> Some t
| _ -> None)
|> String.concat ""
let lines = fullText.Replace("\r\n", "\n").Split('\n')
// 1. Räkna BARA ut minIndent från rader som kommer efter en radbrytning (skippa rad 0)
let minIndent =
if lines.Length <= 1 then 0
if lines.Length <= 1 then
0
else
lines |> Array.skip 1
lines
|> Array.skip 1
|> Array.filter (fun l -> not (System.String.IsNullOrWhiteSpace(l)))
|> Array.map (fun l -> l.Length - l.TrimStart().Length)
|> function [||] -> 0 | arr -> Array.min arr
|> function
| [||] -> 0
| arr -> Array.min arr
let mutable isFirstText = true
let indentStr = "\n" + String.replicate minIndent " "
// 2. Applicera formateringen
let dedented =
nodes |> List.map (function
nodes
|> List.map (function
| Text t ->
let t1 = t.Replace("\r\n", "\n")
@ -95,22 +119,28 @@ module Utils =
if isFirstText then
isFirstText <- false
t1.TrimStart(' ', '\t')
else t1
else
t1
// Ta bort minIndent antal mellanslag efter varje radbrytning i noden
let t3 = if minIndent > 0 then t2.Replace(indentStr, "\n") else t2
Text t3
| otherNode -> otherNode
)
| otherNode -> otherNode)
// 3. Städa bort överflödiga radbrytningar och blanksteg i ytterkanterna
let rec trimStart = function
let rec trimStart =
function
| Text t :: rest ->
let trimmed = t.TrimStart('\n', '\r', ' ', '\t')
if trimmed = "" then trimStart rest else Text trimmed :: rest
if trimmed = "" then
trimStart rest
else
Text trimmed :: rest
| other -> other
let rec trimEnd = function
let rec trimEnd =
function
| Text t :: rest ->
let trimmed = t.TrimEnd('\n', '\r', ' ', '\t')
if trimmed = "" then trimEnd rest else Text trimmed :: rest
@ -126,8 +156,11 @@ module Utils =
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
if index < unnamed.Length then
(snd unnamed.[index]).Trim('"')
else
defaultVal
// 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) =
@ -138,7 +171,13 @@ module Utils =
let withArg1 def (f: string -> InlineNode list -> InlineNode) =
fun _ args children -> f (getArgIdx args 0 def) children
let withArg2 (k1: string) (d1: string) (k2: string) (d2: string) (f: string -> string -> 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
@ -150,14 +189,30 @@ module Utils =
module Parser =
open Ast
let pseudoRandom = new System.Random()
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
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 rec alistGet<'T when 'T: comparison> (lst: ('T * 'B) list) (k: 'T) =
match lst with
| [] -> None
| ktp :: _ when (fst ktp) = k -> Some(snd ktp)
| _ :: rst -> alistGet rst k
let separateArgs rawArgs : (string list * (string * string) list) =
let posArgs = rawArgs |> List.choose (fun (k, v) -> if k = "" then Some v else None)
let kwargs = rawArgs |> List.filter (fun (k, _) -> k <> "")
posArgs, kwargs
let isSection (name: string) = name.EndsWith("section")
@ -165,14 +220,18 @@ module Parser =
let pNamedArg =
// Leta efter "nyckel=värde"
attempt (many1Chars (asciiLetter <|> digit <|> anyOf "-_")
.>> spaces .>> pchar '=' .>> spaces)
attempt (
many1Chars (asciiLetter <|> digit <|> anyOf "-_")
.>> spaces
.>> pchar '='
.>> spaces
)
.>>. manyChars (noneOf ",]")
|>> fun (k, v) -> (k, v.Trim())
|>> fun (k, v) -> k, v.Trim()
let pPositionalArg =
// Bara "värde"
manyChars (noneOf ",]") |>> fun v -> ("", v.Trim())
manyChars (noneOf ",]") |>> fun v -> "", v.Trim()
let pSingleArg = spaces >>. (pNamedArg <|> pPositionalArg) .>> spaces
@ -180,20 +239,19 @@ module Parser =
between (pstring "[") (pstring "]") (sepBy pSingleArg (pchar ','))
>>= fun args ->
// Validera att positionella argument alltid kommer först
let rec validate canBePositional = function
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
else
validate true tail
| _ :: tail -> validate false tail
validate true args
// --- 1. Måsvinge-parser (för @kommandon) ---
// Lägg till en referens för pBody högst upp bland dina referenser
// (Bör ligga precis under let pInline, pInlineRef = ...)
let pBody, pBodyRef = createParserForwardedToRef<InlineNode list, unit> ()
// --- 2. Parentes-parser (för @(...) med sträng-stöd) ---
@ -202,97 +260,148 @@ module Parser =
let pFSharpString =
let normal = many1Chars (noneOf "\"\\")
let escaped = pstring "\\" >>. anyChar |>> sprintf "\\%c"
pstring "\"" .>>. manyStrings (normal <|> escaped) .>>. pstring "\""
|>> fun ((start, inner), end_) -> start + inner + end_
pParenBodyRef.Value <-
manyStrings (choice [
pFSharpString
manyStrings (
choice
[ pFSharpString
many1Chars (noneOf "()\"")
pchar '(' >>. pParenBody .>> pchar ')' |>> sprintf "(%s)"
])
pchar '(' >>. pParenBody .>> pchar ')' |>> sprintf "(%s)" ]
)
let pExpr =
attempt (pstring "@(") >>. pParenBody .>> pstring ")"
|>> fun c -> Expr(c, None)
attempt (pstring "@(") >>. pParenBody .>> pstring ")" |>> fun c -> Expr(c, None)
// --- Övriga inline-parsers ---
let pMultilineCode =
pstring "@\"\"\"" >>. manyCharsTill anyChar (pstring "\"\"\"")
let pInlineCommand =
attempt (pchar '@' >>. many1Chars asciiLetter) .>>. opt pArgs .>>. opt pBody
|>> fun ((name, argsOpt), bodyOpt) ->
let rawArgs = defaultArg argsOpt []
let posArgs, kwargs = separateArgs rawArgs
// dedentNodes fixes indentation of inline commands spanning several lines
let children = defaultArg bodyOpt [] |> Utils.dedentNodes
Command(name, posArgs, Map.ofList (kwargs), children)
let pAtCommand =
pchar '@'
>>. choice
[
// Matchar """ (Eftersom @ redan är konsumerat)
pstring "\"\"\"" >>. manyCharsTill anyChar (pstring "\"\"\"")
|>> fun c -> Expr(c, None)
// pInlineCommand använder nu forward-referensen pBodyRef
let pInlineCommand =
attempt (pchar '@' >>. many1Chars asciiLetter)
.>>. opt pArgs
.>>. opt pBody
// Matchar (
pchar '(' >>. pParenBody .>> pchar ')' |>> fun c -> Expr(c, None)
// Matchar ASCII-bokstäver (för kommandon)
many1Chars asciiLetter .>>. opt pArgs .>>. opt pBody
|>> fun ((name, argsOpt), bodyOpt) ->
let rawArgs = defaultArg argsOpt []
let posArgs = rawArgs |> List.choose (fun (k, v) -> if k = "" then Some v else None)
let kwargs = rawArgs |> List.filter (fun (k, _) -> k <> "") |> Map.ofList
// dedentNodes anropas här från Utils
let children = defaultArg bodyOpt [] |> Utils.dedentNodes
Command(name, posArgs, kwargs, children)
// Nu när pInlineCommand, pExpr och pMultilineCode är definierade
// kan vi skapa pInnerInline
let pInnerInline =
choice [
attempt pInlineCommand
attempt pExpr
pMultilineCode
many1Chars (noneOf "@}") |>> Text
pchar '@' |>> fun _ -> Text "@"
]
// Fallback: det var bara ett löst @ i texten
preturn (Text "@") ]
// / Hjälpare för att bara svälja mellanslag och tabbar (inte radbrytningar)
let pHorizontalSpace = skipMany (anyOf " \t")
let pCommandHead = pchar '@' >>. many1Chars (asciiLetter <|> digit)
let pBlockCommand =
// 1. Tillåt indrag, men inga radbrytningar
pHorizontalSpace >>. pCommandHead .>>. opt pArgs .>>. opt pBody
// 2. MAGIN: Se till att det inte finns mer text raden efter kommandot
.>> pHorizontalSpace
.>> choice [ skipNewline; eof ] // Måste följas av radbrytning eller filslut
|>> fun ((name, argsOpt), bodyOpt) ->
let args, kwargsList = separateArgs (defaultArg argsOpt [])
// Se till att eventuell dedent/städning appliceras här om du har Utils.dedentNodes
let children = defaultArg bodyOpt []
BlockCommand(name, args, Map.ofList kwargsList, children)
let pInnerInline =
choice
[ pAtCommand // Hanterar alla @-baserade noder
many1Chars (noneOf "@}") |>> Text ] // Denna äter råtext ultrasnabbt
// Tilldela värdet till pBodyRef
pBodyRef.Value <- between (pstring "{") (pstring "}") (many pInnerInline)
// Tilldela värdet till pInlineRef
pInlineRef.Value <- choice [
pMultilineCode
pExpr
pInlineCommand
pInlineRef.Value <-
choice
[ pAtCommand
many1Chars (noneOf "@{}\n\r") |>> Text
attempt (pNewline .>> notFollowedBy pNewline) |>> fun _ -> Text "\n"
pchar '@' |>> fun _ -> Text "@"
]
attempt (pNewline .>> notFollowedBy pNewline) |>> fun _ -> Text "\n" ]
// --- Block Parsers ---
let pSectionBlock =
let generateId title maybeid =
let extractIdFromTitle =
function
| Text(t) :: rest -> t.Substring(0, (min 20 t.Length)) + pseudoRandom.GetHexString(8)
| _ -> pseudoRandom.GetHexString(8)
match maybeid with
| Some v -> v
| None -> extractIdFromTitle title
let tryGetId args kwargs =
match args with
| [ id ] -> Some(id)
| _ -> alistGet kwargs "id"
attempt (
pchar '@' >>. many1Chars asciiLetter >>= fun name ->
if isSection name then preturn name
else fail "Inte en sektion."
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 title = defaultArg bodyOpt []
let posArgs, kwargs = separateArgs (defaultArg argsOpt [])
let id = tryGetId posArgs kwargs |> generateId title
let attrs = { id = id; classes = []; kvp = kwargs }
// There are no children in this section yet, since we slurp the children once the
// AST parsing is done
Section(attrs, getSectionLevel name, title, [])
let pParagraphBlock = many1 pInline |>> Paragraph
let pBlock = choice [ pSectionBlock; pParagraphBlock ]
let pBlock = choice [ pSectionBlock; attempt pBlockCommand; pParagraphBlock ]
// --- Dokument Parser ---
let pDocument =
spaces
>>. opt (
pstring "---"
>>. manyCharsTill anyChar (attempt (pNewline >>. pstring "---"))
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
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)
|>> fun (headerOpt, blocks) -> (defaultArg headerOpt Map.empty, blocks)
let parse i =
match run pDocument i with
@ -300,31 +409,25 @@ module Parser =
| Failure(e, _, _) -> failwith e
// ==========================================
// 3. Execution & Printer
// ==========================================
type IEvaluator =
[<AbstractClass>]
type IEvaluator() =
abstract member Evaluate: string -> string
module Execution =
open Ast
let rec transform (metadata: Map<string, string>) (prelude: Map<string, TagRenderer>) (eval: IEvaluator) = function
| 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
| Expr(c, _) -> RawHtml (eval.Evaluate c)
| n -> n
abstract member NewSession: unit -> unit
module Evaluators =
open System.IO
open System.Text
open FSharp.Compiler.Interactive.Shell
type NullEvaluator() =
inherit IEvaluator()
override this.Evaluate(code: string) = ""
override this.NewSession() = ()
type FsiEvaluator() =
inherit IEvaluator()
let sbOut = StringBuilder()
let sbErr = StringBuilder()
let inStream = new StringReader("")
@ -334,10 +437,11 @@ module Evaluators =
// 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) =
let session =
FsiEvaluationSession.Create(fsiConfig, argv, inStream, outStream, errStream)
override this.Evaluate(code: string) =
sbOut.Clear() |> ignore
sbErr.Clear() |> ignore
@ -352,16 +456,22 @@ module Evaluators =
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 "))
)
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.
// Plocka ut värdet. Om det är en sträng, undvik "%A" för a tattecken.
let valStr =
if isNull fsiValue.ReflectionValue then ""
if isNull fsiValue.ReflectionValue then
""
else
match fsiValue.ReflectionValue with
| :? string as s -> s
@ -378,18 +488,129 @@ module Evaluators =
cleanOutput
| Choice2Of2 ex ->
let fsiErrorOutput = if System.String.IsNullOrEmpty(errors) then "Ingen ytterligare FSI-output." else errors
sprintf """
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
</div>"""
code
ex.Message
fsiErrorOutput
with ex ->
sprintf """<div style="color: red; border: 1px solid red; padding: 10px;">
sprintf
"""<div style="color: red; border: 1px solid red; padding: 10px;">
<strong>Kritiskt FSI-systemfel:</strong> %s
</div>""" ex.Message
</div>"""
ex.Message
override this.NewSession() =
this.Evaluate($"module {Random.Shared.GetHexString(10)}")
()
module CommandEvaluator =
open Ast
// Interface för att utvärdera Expr-noder (kodblock/matte)
// Huvudfunktionen
let rec evaluateDocument
(metadata: Map<string, string>)
(prelude: Map<string, TagRenderer>)
(exprEval: IEvaluator)
(blocks: BlockNode list)
=
blocks |> List.map (evaluateBlock metadata prelude exprEval)
// Evaluerar Block-noder
and evaluateBlock metadata prelude exprEval block =
let evalBlocks = evaluateDocument metadata prelude exprEval
let evalInlines = List.map (evaluateInline metadata prelude exprEval)
match block with
| CodeBlock(attr, text) -> CodeBlock(attr, text)
| Figure(attr, caption, blocks) -> Figure(attr, evalInlines caption, evalBlocks blocks)
| Plain inlines -> Plain(evalInlines inlines)
| Paragraph inlines -> Paragraph(evalInlines inlines)
| Section(attr, lvl, title, body) -> Section(attr, lvl, evalInlines title, evalBlocks body)
| ListBlock kind ->
match kind with
| Orderedlist(attr, start, blocksList) ->
Orderedlist(attr, start, blocksList |> List.map evalBlocks) |> ListBlock
| BulletList(attr, blocksList) -> BulletList(attr, blocksList |> List.map evalBlocks) |> ListBlock
// TODO: This should run recursively to make sure a command can return a new command!
| BlockCommand(name, args, kwargs, children) ->
let evalChildren = children |> List.map (evaluateInline metadata prelude exprEval)
match prelude.TryFind name with
| Some renderer ->
// Om du ändrar TagRenderer att returnera BlockNode direkt är det ännu bättre.
match renderer metadata args kwargs evalChildren with
| Block i -> i // Exempel: @table blev en riktig Table-nod
| Inline i -> Plain [ i ]
| None -> failwithf "Okänt block-kommando: %s" name
// Evaluerar Inline-noder
and evaluateInline metadata prelude exprEval inlineNode =
let evalInlines = List.map (evaluateInline metadata prelude exprEval)
let evalBlocks = evaluateDocument metadata prelude exprEval
match inlineNode with
// Noder som inte har några barn eller commands i sig
| Text _
| RawHtml _
| SoftBreak
| LineBreak -> inlineNode
// Enkla wrappers som bara skickar vidare barnen
| Emph children -> Emph(evalInlines children)
| Underline children -> Underline(evalInlines children)
| Strong children -> Strong(evalInlines children)
| Strikeout children -> Strikeout(evalInlines children)
| Superscript children -> Superscript(evalInlines children)
| Subscript children -> Subscript(evalInlines children)
// Komplexa noder
| Link(attr, target) ->
let url, content = target
Link(attr, (url, evalInlines content))
| Image(attr, altText, url) -> Image(attr, evalInlines altText, url)
| Code(attr, text) -> Code(attr, text)
| Note blocks ->
// Här går vi tillbaka till att evaluera block!
Note(evalBlocks blocks)
| Expr(code, res) -> Expr(code, Some(exprEval.Evaluate code))
// TODO: This should run recursively to make sure a command can return a new command!
| Command(tag, args, kwargs, children) ->
// 1. Evaluera barnen först (Bottom-Up)
let evaluatedChildren = evalInlines children
// 2. Slå upp kommandot i preluden
match prelude.TryFind tag with
| Some renderer ->
// 3. Kör funktionen och returnera den nya noden
match renderer metadata args kwargs evaluatedChildren with
| Inline i -> i
| Block b -> failwithf "Syntaxfel: Blocknod i inline-kontext: %A" b
| None -> failwithf "Syntaxfel: Okänt kommando '@%s' hittades under evaluering." tag

View file

@ -7,14 +7,16 @@ module Pandoc =
open System.Diagnostics
let toHtml (from: string) (markdownText: string) =
let startInfo = ProcessStartInfo(
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)
CreateNoWindow = true
)
try
use proc = Process.Start startInfo
@ -30,6 +32,7 @@ module Pandoc =
proc.WaitForExit()
Console.WriteLine(htmlOutput)
if proc.ExitCode = 0 then
htmlOutput.Trim()
else

View file

@ -1,26 +1,48 @@
namespace Fibble.FibLib
open FSharp.Compiler.Text
open Fibble.FibLib.Ast
module Helpers =
let emptyAttr = { id = ""; classes = []; kvp = [] }
let ah cls kvp =
{id=""; classes = [cls]; kvp = Map.toList kvp }
{ id = ""
classes = [ cls ]
kvp = Map.toList kvp }
let onlyChildren constructor : TagRenderer =
fun _ _ _ children -> constructor(children)
fun _ _ _ children -> Inline(constructor (children))
let makeAttr kwargs =
let id = defaultArg (Map.tryFind "id" kwargs) ""
let classes =
match Map.tryFind "class" kwargs with
| Some v -> v.Split "," |> Array.toList
| None -> []
let rest =
Map.toList kwargs
|> List.filter (fun p -> not (List.contains (fst p) [ "id"; "class" ]))
{ id = id
classes = classes
kvp = rest }
open Helpers
module ConstructionHelpers =
let linebreak _ _ _ _ = LineBreak
let softbreak _ _ _ _ = SoftBreak
let linebreak _ _ _ _ = Inline(LineBreak)
let softbreak _ _ _ _ = Inline(SoftBreak)
let value: TagRenderer =
fun meta args _ _ ->
match Map.tryFind args.Head meta with
| Some(v) -> Text v
| None -> Text $"value {args.Head} not found in metadata"
| Some v -> Inline(Text v)
| None -> Inline(Text $"value {args.Head} not found in metadata")
let emph = onlyChildren Emph
@ -33,21 +55,36 @@ module ConstructionHelpers =
let image: TagRenderer =
fun _ args kwargs children ->
let attributes = ah "inlineImage" kwargs
Image(attributes,children, args[0])
Inline(Image(attributes, children, args[0]))
let code: TagRenderer =
fun _ _ kwargs children ->
let attributes = ah "inlineCode" kwargs
match children with
| [Text(c)] -> InlineNode.Code(attributes, c)
| [ Text(c) ] -> Inline(InlineNode.Code(attributes, c))
| _ -> failwith "Code tag was not Text,"
let link: TagRenderer =
fun _ args kwargs children ->
let attributes = ah "link" kwargs
Link(attributes,Target(args[0], children))
Inline(Link(attributes, Target(args[0], children)))
// blocks
let paragraph _ _ _ children = Paragraph(children)
let plain _ _ _ children = Plain(children)
let table _ args kwargs children =
let rows =
children
|> AstUtils.splitByNewlines
|> List.map (fun rowNodes ->
let cells = rowNodes |> AstUtils.splitByPipe |> List.map Cell // Wrap each split list in a Cell node
Row cells)
let attr = makeAttr kwargs
Block(Table(attr, rows))