Skip to content

Instantly share code, notes, and snippets.

@deviousasti
Created May 7, 2020 14:14
Show Gist options
  • Save deviousasti/b63f98b66401676511f00148ae38ce8f to your computer and use it in GitHub Desktop.
Save deviousasti/b63f98b66401676511f00148ae38ce8f to your computer and use it in GitHub Desktop.
Html Imports Bundler
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp3.1</TargetFramework>
<RootNamespace>html_imports</RootNamespace>
</PropertyGroup>
<ItemGroup>
<Compile Include="Html.fs" />
<Compile Include="Program.fs" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="HtmlAgilityPack" Version="1.11.23" />
</ItemGroup>
</Project>
module HtmlAgilityPack.FSharp
open HtmlAgilityPack
let parent (node : HtmlNode) =
node.ParentNode
let element name (node : HtmlNode) =
node.Element name
let elements name (node : HtmlNode) =
node.Elements name
let children (node : HtmlNode) =
node.ChildNodes |> Seq.cast<HtmlNode>
let descendants name (node : HtmlNode) =
node.Descendants (name = name)
let descendantsAndSelf name (node : HtmlNode) =
node.DescendantsAndSelf name
let ancestors name (node : HtmlNode) =
node.Ancestors name
let ancestorsAndSelf name (node : HtmlNode) =
node.AncestorsAndSelf name
let inline attr name (node : HtmlNode) =
node.GetAttributeValue(name, "")
let inline attrMap name map (node : HtmlNode) =
let clone = node.Clone()
clone.SetAttributeValue(name, map (node.GetAttributeValue(name, ""))) |> ignore
clone
let inline name (node : HtmlNode) =
node.Name
let inline hasAttr name value node =
attr name node = value
let inline hasText value (node : HtmlNode) =
node.InnerText = value
let createDoc html =
let doc = new HtmlDocument()
doc.LoadHtml html
doc
let rootNode (doc:HtmlDocument) =
doc.DocumentNode
let isValid (doc:HtmlDocument) =
doc.ParseErrors |> Seq.isEmpty
open System
open System.IO
open HtmlAgilityPack.FSharp
open HtmlAgilityPack
open System.Collections.Generic
let log value = printfn "%A" value
let warnWith text =
Console.ForegroundColor <- ConsoleColor.Yellow
Console.WriteLine(string text)
Console.ResetColor()
type ImportDocument = { document: HtmlDocument; file: string }
let fullPath = Path.GetFullPath
let parentDir file =
Path.GetDirectoryName(fullPath file)
let relativeTo file other =
Path.Combine((parentDir file), other) |> fullPath
let partialRelativeTo root file =
Path.GetRelativePath((parentDir root), file).Replace("\\", "/")
let someIf condition value = if condition then Some value else None
let (|Import|_|) node = someIf ((node |> name = "link") && (node |> attr "rel" = "import")) node
let (|Script|_|) node = someIf ((node |> name = "script") && (node |> attr "src" <> "")) node
let (|Style|_|) node = someIf ((node |> name = "link") && (node |> attr "rel" = "stylesheet")) node
let scanFile file =
try
let doc = (createDoc (File.ReadAllText file))
Some { document = doc; file = file; }
with ex ->
warnWith (sprintf "Could not parse: %s\n%A" file ex)
None
let unfoldImports source root hasResource =
let rec unfold source rel =
let importfile = source |> attr "href" |> relativeTo rel
let relativeToImport rel = rel |> relativeTo importfile |> partialRelativeTo root.file
let imported = if hasResource importfile then None else scanFile importfile
match imported with
| Some(imported) ->
seq {
for elem in imported.document |> rootNode |> children do
match elem with
| Import(elem) -> yield! unfold elem importfile
| Script(elem) -> yield elem |> attrMap "src" relativeToImport
| Style(elem) -> yield elem |> attrMap "href" relativeToImport
| _ -> yield elem
}
| None -> Seq.empty
unfold source root.file
let replaceImports doc =
let set = new HashSet<_>()
let add = not << set.Add
let replaceImport source =
unfoldImports source doc add
|> Seq.fold (fun (cur: HtmlNode) elem -> cur.ParentNode.InsertAfter(elem, cur)) source
|> ignore
source.Remove()
doc.document
|> rootNode
|> descendants "link"
|> Seq.choose (|Import|_|)
|> Seq.toArray
|> Seq.iter replaceImport
[<EntryPoint>]
let main argv =
let input, output =
match argv with
| [||] -> failwith "Input file not specified"
| [| input |] -> input, Console.Out
| [| input; output |] -> input, new StreamWriter(File.OpenWrite(output)) :> TextWriter
| other -> failwith (sprintf "Unknown arguments: %A" other)
match scanFile input with
| None -> printfn "Input file was invalid"
| Some (root) ->
replaceImports root
root.document.Save(output)
0 // return an integer exit code
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment