Compare commits
14 Commits
fsharp
...
pictureEle
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a3838b0f4d | ||
|
|
38f82061e9 | ||
|
|
57a8bba66a | ||
|
|
43d6e33638 | ||
|
|
d875ca6fea | ||
|
|
d846a538a0 | ||
|
|
00b711aef4 | ||
|
|
dbb6ae208b | ||
|
|
de62e6275d | ||
|
|
d873be97d8 | ||
|
|
f3080faae0 | ||
|
|
6ed0ef4205 | ||
|
|
ab9250b968 | ||
|
|
c24684fa8b |
17
.vscode/launch.json
vendored
17
.vscode/launch.json
vendored
@@ -1,18 +1,17 @@
|
||||
{
|
||||
// Use IntelliSense to find out which attributes exist for C# debugging
|
||||
// Use hover for the description of the existing attributes
|
||||
// For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": ".NET Core Launch (web)",
|
||||
"type": "coreclr",
|
||||
"request": "launch",
|
||||
"preLaunchTask": "build",
|
||||
// If you have changed target frameworks, make sure to update the program path.
|
||||
"program": "${workspaceFolder}/TerribleDev.Blog.Web/bin/Debug/netcoreapp2.2/TerribleDev.Blog.Web.dll",
|
||||
"program": "${workspaceFolder}/src/TerribleDev.Blog.Web/bin/Debug/netcoreapp2.2/TerribleDev.Blog.Web.dll",
|
||||
"args": [],
|
||||
"cwd": "${workspaceFolder}/TerribleDev.Blog.Web",
|
||||
"cwd": "${workspaceFolder}/src/TerribleDev.Blog.Web",
|
||||
"stopAtEntry": false,
|
||||
"internalConsoleOptions": "openOnSessionStart",
|
||||
"launchBrowser": {
|
||||
@@ -42,5 +41,5 @@
|
||||
"request": "attach",
|
||||
"processId": "${command:pickProcess}"
|
||||
}
|
||||
,]
|
||||
]
|
||||
}
|
||||
2
.vscode/tasks.json
vendored
2
.vscode/tasks.json
vendored
@@ -7,7 +7,7 @@
|
||||
"type": "process",
|
||||
"args": [
|
||||
"build",
|
||||
"${workspaceFolder}/TerribleDev.Blog.Web/TerribleDev.Blog.Web.csproj"
|
||||
"${workspaceFolder}/src/TerribleDev.Blog.Web/TerribleDev.Blog.Web.csproj"
|
||||
],
|
||||
"problemMatcher": "$msCompile"
|
||||
}
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace TerribleDev.Blog.MarkdownPlugins
|
||||
{
|
||||
public static class StringExtension
|
||||
{
|
||||
public static string WithoutSpecialCharacters(this string str)
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
foreach (char c in str)
|
||||
{
|
||||
if ((c >= '0' && c <= '9') || (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || c == '_' || c == '-')
|
||||
{
|
||||
sb.Append(c);
|
||||
}
|
||||
}
|
||||
return sb.ToString();
|
||||
}
|
||||
public static string RemoveSpecialCharacters(string str)
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
foreach (char c in str)
|
||||
{
|
||||
if ((c >= '0' && c <= '9') || (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || c == '_' || c == '-')
|
||||
{
|
||||
sb.Append(c);
|
||||
}
|
||||
}
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
namespace TerribleDev.Blog.MarkdownPlugins
|
||||
{
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Markdig;
|
||||
using Markdig.Renderers;
|
||||
using Markdig.Renderers.Html;
|
||||
using Markdig.Renderers.Html.Inlines;
|
||||
using Markdig.Syntax.Inlines;
|
||||
|
||||
/// <summary>
|
||||
/// Extension for extending image Markdown links in case a video or an audio file is linked and output proper link.
|
||||
/// </summary>
|
||||
/// <seealso cref="Markdig.IMarkdownExtension" />
|
||||
public class TargetLinkExtension : IMarkdownExtension
|
||||
{
|
||||
|
||||
public void Setup(MarkdownPipelineBuilder pipeline)
|
||||
{
|
||||
}
|
||||
|
||||
public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer)
|
||||
{
|
||||
var htmlRenderer = renderer as HtmlRenderer;
|
||||
if (htmlRenderer != null)
|
||||
{
|
||||
var inlineRenderer = htmlRenderer.ObjectRenderers.FindExact<LinkInlineRenderer>();
|
||||
if (inlineRenderer != null)
|
||||
{
|
||||
inlineRenderer.TryWriters.Remove(TryLinkInlineRenderer);
|
||||
inlineRenderer.TryWriters.Add(TryLinkInlineRenderer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryLinkInlineRenderer(HtmlRenderer renderer, LinkInline linkInline)
|
||||
{
|
||||
if (linkInline.Url == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Uri uri;
|
||||
// Only process absolute Uri
|
||||
if (!Uri.TryCreate(linkInline.Url, UriKind.RelativeOrAbsolute, out uri) || !uri.IsAbsoluteUri)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
RenderTargetAttribute(uri, renderer, linkInline);
|
||||
return false;
|
||||
}
|
||||
|
||||
private void RenderTargetAttribute(Uri uri, HtmlRenderer renderer, LinkInline linkInline)
|
||||
{
|
||||
|
||||
linkInline.SetAttributes(new HtmlAttributes() { Properties = new List<KeyValuePair<string, string>>() { new KeyValuePair<string, string>("target", "_blank"), new KeyValuePair<string, string>("rel", "noopener"), } });
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
namespace TerribleDev.Blog.MarkdownPlugins
|
||||
{
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Markdig;
|
||||
using Markdig.Renderers;
|
||||
using Markdig.Renderers.Html;
|
||||
using Markdig.Renderers.Html.Inlines;
|
||||
using Markdig.Syntax.Inlines;
|
||||
|
||||
public class ImageRecorder : IMarkdownExtension
|
||||
{
|
||||
private List<string> images;
|
||||
public ImageRecorder(List<string> images)
|
||||
{
|
||||
this.images = images;
|
||||
}
|
||||
public void Setup(MarkdownPipelineBuilder pipeline)
|
||||
{
|
||||
}
|
||||
|
||||
public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer)
|
||||
{
|
||||
var htmlRenderer = renderer as HtmlRenderer;
|
||||
if (htmlRenderer != null)
|
||||
{
|
||||
var inlineRenderer = htmlRenderer.ObjectRenderers.FindExact<LinkInlineRenderer>();
|
||||
if (inlineRenderer != null)
|
||||
{
|
||||
inlineRenderer.TryWriters.Remove(TryLinkInlineRenderer);
|
||||
inlineRenderer.TryWriters.Add(TryLinkInlineRenderer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryLinkInlineRenderer(HtmlRenderer renderer, LinkInline linkInline)
|
||||
{
|
||||
if (linkInline.Url == null || !linkInline.IsImage)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
this.images.Add(linkInline.Url);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netstandard2.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Markdig" Version="0.15.7" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -7,10 +7,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{E6C01762-AEB
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TerribleDev.Blog.Web", "src\TerribleDev.Blog.Web\TerribleDev.Blog.Web.csproj", "{BAA8662D-6D38-4811-A6FF-7A61D0C633D2}"
|
||||
EndProject
|
||||
Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "TerribleDev.Blog.Core", "src\TerribleDev.Blog.Core\TerribleDev.Blog.Core.fsproj", "{31876934-45BC-4C28-BBAF-0A50047345BB}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TerribleDev.Blog.MarkdownPlugins", "TerribleDev.Blog.MarkdownPlugins\TerribleDev.Blog.MarkdownPlugins.csproj", "{C3A2BFB5-64FC-4FCF-A26C-B4E4C67531B7}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
@@ -33,38 +29,12 @@ Global
|
||||
{BAA8662D-6D38-4811-A6FF-7A61D0C633D2}.Release|x64.Build.0 = Release|Any CPU
|
||||
{BAA8662D-6D38-4811-A6FF-7A61D0C633D2}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{BAA8662D-6D38-4811-A6FF-7A61D0C633D2}.Release|x86.Build.0 = Release|Any CPU
|
||||
{31876934-45BC-4C28-BBAF-0A50047345BB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{31876934-45BC-4C28-BBAF-0A50047345BB}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{31876934-45BC-4C28-BBAF-0A50047345BB}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{31876934-45BC-4C28-BBAF-0A50047345BB}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{31876934-45BC-4C28-BBAF-0A50047345BB}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{31876934-45BC-4C28-BBAF-0A50047345BB}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{31876934-45BC-4C28-BBAF-0A50047345BB}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{31876934-45BC-4C28-BBAF-0A50047345BB}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{31876934-45BC-4C28-BBAF-0A50047345BB}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{31876934-45BC-4C28-BBAF-0A50047345BB}.Release|x64.Build.0 = Release|Any CPU
|
||||
{31876934-45BC-4C28-BBAF-0A50047345BB}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{31876934-45BC-4C28-BBAF-0A50047345BB}.Release|x86.Build.0 = Release|Any CPU
|
||||
{C3A2BFB5-64FC-4FCF-A26C-B4E4C67531B7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{C3A2BFB5-64FC-4FCF-A26C-B4E4C67531B7}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{C3A2BFB5-64FC-4FCF-A26C-B4E4C67531B7}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{C3A2BFB5-64FC-4FCF-A26C-B4E4C67531B7}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{C3A2BFB5-64FC-4FCF-A26C-B4E4C67531B7}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{C3A2BFB5-64FC-4FCF-A26C-B4E4C67531B7}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{C3A2BFB5-64FC-4FCF-A26C-B4E4C67531B7}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{C3A2BFB5-64FC-4FCF-A26C-B4E4C67531B7}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{C3A2BFB5-64FC-4FCF-A26C-B4E4C67531B7}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{C3A2BFB5-64FC-4FCF-A26C-B4E4C67531B7}.Release|x64.Build.0 = Release|Any CPU
|
||||
{C3A2BFB5-64FC-4FCF-A26C-B4E4C67531B7}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{C3A2BFB5-64FC-4FCF-A26C-B4E4C67531B7}.Release|x86.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(NestedProjects) = preSolution
|
||||
{BAA8662D-6D38-4811-A6FF-7A61D0C633D2} = {E6C01762-AEBF-47C4-8D95-383504D8BC70}
|
||||
{31876934-45BC-4C28-BBAF-0A50047345BB} = {E6C01762-AEBF-47C4-8D95-383504D8BC70}
|
||||
{C3A2BFB5-64FC-4FCF-A26C-B4E4C67531B7} = {E6C01762-AEBF-47C4-8D95-383504D8BC70}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {CFA796F1-4389-452F-B224-E64C72E907C4}
|
||||
|
||||
7
docker-compose.yml
Normal file
7
docker-compose.yml
Normal file
@@ -0,0 +1,7 @@
|
||||
version: '3'
|
||||
services:
|
||||
webapp:
|
||||
build: ./src/TerribleDev.Blog.Web
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
@@ -1,85 +0,0 @@
|
||||
namespace TerribleDev.Blog.Core.Factories
|
||||
module BlogFactory =
|
||||
|
||||
open System.IO
|
||||
open YamlDotNet.Serialization
|
||||
open TerribleDev.Blog.Core.Models
|
||||
open TerribleDev.Blog.Core
|
||||
open Markdig
|
||||
open TerribleDev.Blog.MarkdownPlugins
|
||||
open Microsoft.AspNetCore.Html
|
||||
open System.Linq
|
||||
open System.Collections.Generic
|
||||
open fs
|
||||
open System
|
||||
|
||||
let fixTagName (tag:string) = tag.Replace(' ', '-').WithoutSpecialCharacters().ToLower()
|
||||
let mapImgUrlResolver (resolveUrl:string) =
|
||||
fun (imgUrl: string) -> if imgUrl.StartsWith('/') then imgUrl else sprintf "/%s/%s" resolveUrl imgUrl
|
||||
|
||||
|
||||
let getPosts (path) = Directory.EnumerateFiles(path, "*.md", SearchOption.TopDirectoryOnly)
|
||||
let parseYml (postText:string) =
|
||||
let split = postText.Split("---")
|
||||
match split with
|
||||
| [| _ |] -> raise(Exception("No yml found"))
|
||||
| [| yml; _|] -> DeserializerBuilder().Build().Deserialize(yml)
|
||||
| split when split.Length > 2 -> DeserializerBuilder().Build().Deserialize(split.[0])
|
||||
|
||||
|
||||
|
||||
let getMarkdownBuilder (imgRef) =
|
||||
MarkdownPipelineBuilder()
|
||||
.Use<TargetLinkExtension>()
|
||||
.Use<ImageRecorder>(new ImageRecorder(imgRef))
|
||||
.UseMediaLinks()
|
||||
.UseEmojiAndSmiley()
|
||||
.Build()
|
||||
let parsePost (postText:string, fileName:FileInfo, postSettings:PostSettings): Post =
|
||||
let mutable images = System.Collections.Generic.List<string>()
|
||||
let markdownBuilder = getMarkdownBuilder(images)
|
||||
//todo this function is a bit gross
|
||||
let markdownText = postText.Split("---") |> Seq.skip 1 |> String.concat ""
|
||||
let postContent = Markdown.ToHtml(markdownText, markdownBuilder);
|
||||
let postContentPlain = Markdown.ToPlainText(markdownText, markdownBuilder).Split("<!-- more -->") |> String.concat ""
|
||||
//todo pattern match
|
||||
let resolvedUrl = if System.String.IsNullOrWhiteSpace(postSettings.permalink) then fileName.Name.Split('.').[0].Replace(' ', '-').WithoutSpecialCharacters() else postSettings.permalink
|
||||
let summary = postContent.Split("<!-- more -->").[0];
|
||||
let postSummaryPlain = postContentPlain.Split("<!-- more -->").[0];
|
||||
let tags = match postSettings.tags with
|
||||
| null -> Seq.empty
|
||||
| x -> x |> Seq.map fixTagName
|
||||
let summaryPlainShort = match postContentPlain with
|
||||
| postContentPlain when postContentPlain.Length <= 147 -> postContentPlain
|
||||
| postContentPlain -> postContentPlain.Substring(0, 146) + "..."
|
||||
let mapImgUrlFromResolved = mapImgUrlResolver resolvedUrl
|
||||
let images = images |> Seq.distinct |> Seq.map mapImgUrlFromResolved |> Seq.toList
|
||||
{
|
||||
PublishDate = postSettings.date.ToUniversalTime();
|
||||
tags = tags |> Seq.toList
|
||||
Title = postSettings.title;
|
||||
Url = resolvedUrl;
|
||||
Content = HtmlString(postContent);
|
||||
Summary = HtmlString(summary);
|
||||
SummaryPlain = postSummaryPlain;
|
||||
SummaryPlainShort = summaryPlainShort;
|
||||
ContentPlain = postContentPlain;
|
||||
Images = images |> Seq.toList
|
||||
}
|
||||
let getAllPosts path =
|
||||
getPosts path
|
||||
|> Seq.map getFileInfo
|
||||
|> Seq.map(Async.map(fun (text, fileInfo) -> (text, fileInfo, parseYml(text))))
|
||||
|> Seq.map(Async.map(parsePost))
|
||||
|> Async.Parallel
|
||||
// todo stop this
|
||||
|> Async.RunSynchronously
|
||||
|> System.Collections.Generic.List
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
namespace TerribleDev.Blog.Core.Models
|
||||
open System;
|
||||
open Microsoft.AspNetCore.Html;
|
||||
|
||||
type Post =
|
||||
{
|
||||
Url: string;
|
||||
Title: string;
|
||||
PublishDate: DateTime;
|
||||
Content: HtmlString;
|
||||
Summary: HtmlString;
|
||||
ContentPlain: string;
|
||||
SummaryPlain: string;
|
||||
SummaryPlainShort: string;
|
||||
tags : List<string>
|
||||
Images: List<string>
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
namespace TerribleDev.Blog.Core.Models
|
||||
open System;
|
||||
open System.Collections.Generic
|
||||
|
||||
[<CLIMutable>]
|
||||
type PostSettings =
|
||||
{
|
||||
tags: List<string>;
|
||||
title: string
|
||||
permalink: string
|
||||
date: DateTime
|
||||
updated: DateTime
|
||||
id:string
|
||||
thumbnail_image:string
|
||||
thumbnailImage:string
|
||||
thumbnail_image_position:string
|
||||
layout:string
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
namespace TerribleDev.Blog.Core.Models
|
||||
open Microsoft.SyndicationFeed
|
||||
open System
|
||||
|
||||
module Util =
|
||||
let ToSyndicationItem (x: Post) =
|
||||
let url = sprintf "https://blog.terribledev.io/%s" x.Url
|
||||
let publishDate : DateTimeOffset = (DateTimeOffset) x.PublishDate
|
||||
SyndicationItem(Title = x.Title, Description = x.Content.ToString(), Id = url, Published = publishDate)
|
||||
@@ -1,23 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netcoreapp2.2</TargetFramework>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="Util\fs.fs" />
|
||||
<Compile Include="Util\Constants.fs" />
|
||||
<Compile Include="Util\Async.fs" />
|
||||
<Compile Include="Models\PostSettings.fs" />
|
||||
<Compile Include="Models\Post.fs" />
|
||||
<Compile Include="Models\Util.fs" />
|
||||
<Compile Include="Factories\BlogFactory.fs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.SyndicationFeed.ReaderWriter" Version="1.0.2" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Html.Abstractions" Version="2.2.0" />
|
||||
<PackageReference Include="Markdig" Version="0.15.7" />
|
||||
<PackageReference Include="YamlDotNet" Version="5.3.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\TerribleDev.Blog.MarkdownPlugins\TerribleDev.Blog.MarkdownPlugins.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -1,35 +0,0 @@
|
||||
module Async
|
||||
|
||||
let map f xAsync = async {
|
||||
// get the contents of xAsync
|
||||
let! x = xAsync
|
||||
// apply the function and lift the result
|
||||
return f x
|
||||
}
|
||||
|
||||
let retn x = async {
|
||||
// lift x to an Async
|
||||
return x
|
||||
}
|
||||
|
||||
let apply fAsync xAsync = async {
|
||||
// start the two asyncs in parallel
|
||||
let! fChild = Async.StartChild fAsync
|
||||
let! xChild = Async.StartChild xAsync
|
||||
|
||||
// wait for the results
|
||||
let! f = fChild
|
||||
let! x = xChild
|
||||
|
||||
// apply the function to the results
|
||||
return f x
|
||||
}
|
||||
|
||||
let bind f xAsync = async {
|
||||
// get the contents of xAsync
|
||||
let! x = xAsync
|
||||
// apply the function but don't lift the result
|
||||
// as f will return an Async
|
||||
return! f x
|
||||
}
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
module Constants
|
||||
module Constants=
|
||||
|
||||
let MORE = "<!-- more -->"
|
||||
let YMLDIVIDER = "---"
|
||||
type Tokens =
|
||||
| MORE
|
||||
| YMLDIVIDER
|
||||
@@ -1,10 +0,0 @@
|
||||
module fs
|
||||
|
||||
open System.IO
|
||||
|
||||
let getFileInfo (filePath:string) =
|
||||
let fileInfo = FileInfo(filePath)
|
||||
async {
|
||||
let! text = File.ReadAllTextAsync(fileInfo.FullName) |> Async.AwaitTask
|
||||
return (text, fileInfo)
|
||||
}
|
||||
@@ -12,10 +12,10 @@ namespace TerribleDev.Blog.Web.Controllers
|
||||
{
|
||||
public class HomeController : Controller
|
||||
{
|
||||
public static List<TerribleDev.Blog.Core.Models.Post> postsAsList = TerribleDev.Blog.Core.Factories.BlogFactory.getAllPosts("Posts").OrderByDescending(a => a.PublishDate).ToList();
|
||||
public static Dictionary<string, List<TerribleDev.Blog.Core.Models.Post>> tagToPost = postsAsList.Where(a=>a.tags != null)
|
||||
public static List<IPost> postsAsList = new BlogFactory().GetAllPosts().OrderByDescending(a=>a.PublishDate).ToList();
|
||||
public static Dictionary<string, List<IPost>> tagToPost = postsAsList.Where(a=>a.tags != null)
|
||||
.Aggregate(
|
||||
new Dictionary<string, List<TerribleDev.Blog.Core.Models.Post>>(),
|
||||
new Dictionary<string, List<IPost>>(),
|
||||
(accum, item) => {
|
||||
foreach(var tag in item.tags)
|
||||
{
|
||||
@@ -25,19 +25,19 @@ namespace TerribleDev.Blog.Web.Controllers
|
||||
}
|
||||
else
|
||||
{
|
||||
accum[tag] = new List<TerribleDev.Blog.Core.Models.Post>() { item };
|
||||
accum[tag] = new List<IPost>() { item };
|
||||
}
|
||||
}
|
||||
return accum;
|
||||
});
|
||||
public static IDictionary<string, TerribleDev.Blog.Core.Models.Post> posts = postsAsList.ToDictionary(a => a.Url);
|
||||
public static IDictionary<int, List<TerribleDev.Blog.Core.Models.Post>> postsByPage = postsAsList.Aggregate(new Dictionary<int, List<TerribleDev.Blog.Core.Models.Post>>() { [1] = new List<TerribleDev.Blog.Core.Models.Post>() }, (accum, item) =>
|
||||
public static IDictionary<string, IPost> posts = postsAsList.ToDictionary(a => a.Url);
|
||||
public static IDictionary<int, List<IPost>> postsByPage = postsAsList.Aggregate(new Dictionary<int, List<IPost>>() { [1] = new List<IPost>() }, (accum, item) =>
|
||||
{
|
||||
var highestPage = accum.Keys.Max();
|
||||
var current = accum[highestPage].Count;
|
||||
if (current >= 10)
|
||||
{
|
||||
accum[highestPage + 1] = new List<TerribleDev.Blog.Core.Models.Post>() { item };
|
||||
accum[highestPage + 1] = new List<IPost>() { item };
|
||||
return accum;
|
||||
}
|
||||
accum[highestPage].Add(item);
|
||||
@@ -47,6 +47,7 @@ namespace TerribleDev.Blog.Web.Controllers
|
||||
[Route("/")]
|
||||
[Route("/index.html")]
|
||||
[Route("/page/{pageNumber?}" )]
|
||||
[OutputCache(Duration = 31536000, VaryByParam = "pageNumber")]
|
||||
public IActionResult Index(int pageNumber = 1)
|
||||
{
|
||||
if(!postsByPage.TryGetValue(pageNumber, out var result))
|
||||
|
||||
@@ -15,12 +15,18 @@ namespace TerribleDev.Blog.Web.Controllers
|
||||
{
|
||||
public class SeoController : Controller
|
||||
{
|
||||
private readonly BlogConfiguration configuration;
|
||||
public SeoController(BlogConfiguration configuration)
|
||||
{
|
||||
this.configuration = configuration;
|
||||
|
||||
}
|
||||
public static DateTimeOffset publishDate = DateTimeOffset.UtcNow; // keep publish date in memory so we just return when the server was kicked
|
||||
public static IEnumerable<SyndicationItem> postsToSyndication = HomeController.postsAsList.Select(Core.Models.Util.ToSyndicationItem).ToList();
|
||||
public static IEnumerable<SyndicationItem> postsToSyndication = HomeController.postsAsList.Select(a => a.ToSyndicationItem()).ToList();
|
||||
[Route("/rss")]
|
||||
[Route("/rss.xml")]
|
||||
[ResponseCache(Duration = 7200)]
|
||||
[OutputCache(Duration = 86400)]
|
||||
[OutputCache(Duration = 31536000)]
|
||||
public async Task Rss()
|
||||
{
|
||||
Response.StatusCode = 200;
|
||||
@@ -28,8 +34,8 @@ namespace TerribleDev.Blog.Web.Controllers
|
||||
using (XmlWriter xmlWriter = XmlWriter.Create(this.Response.Body, new XmlWriterSettings() { Async = true, Indent = false, Encoding = Encoding.UTF8 }))
|
||||
{
|
||||
var writer = new RssFeedWriter(xmlWriter);
|
||||
await writer.WriteTitle("The Ramblings of TerribleDev");
|
||||
await writer.WriteValue("link", "https://blog.terribledev.io");
|
||||
await writer.WriteTitle(configuration.Title);
|
||||
await writer.WriteValue("link", configuration.Link);
|
||||
await writer.WriteDescription("My name is Tommy Parnell. I usually go by TerribleDev on the internets. These are just some of my writings and rants about the software space.");
|
||||
|
||||
foreach (var item in postsToSyndication)
|
||||
@@ -43,12 +49,12 @@ namespace TerribleDev.Blog.Web.Controllers
|
||||
}
|
||||
[Route("/sitemap.xml")]
|
||||
[ResponseCache(Duration = 7200)]
|
||||
[OutputCache(Duration = 86400)]
|
||||
[OutputCache(Duration = 31536000)]
|
||||
public void SiteMap()
|
||||
{
|
||||
Response.StatusCode = 200;
|
||||
Response.ContentType = "text/xml";
|
||||
var sitewideLinks = new List<SiteMapItem>(HomeController.tagToPost.Keys.Select(a=> new SiteMapItem() { LastModified = DateTime.UtcNow, Location = $"https://blog.terribledev.io/tag/{a}/"}))
|
||||
var sitewideLinks = new List<SiteMapItem>(HomeController.tagToPost.Keys.Select(a => new SiteMapItem() { LastModified = DateTime.UtcNow, Location = $"https://blog.terribledev.io/tag/{a}/" }))
|
||||
{
|
||||
new SiteMapItem() { LastModified = DateTime.UtcNow, Location="https://blog.terribledev.io/all-tags/" }
|
||||
};
|
||||
|
||||
23
src/TerribleDev.Blog.Web/Extensions/IPostExtensions.cs
Normal file
23
src/TerribleDev.Blog.Web/Extensions/IPostExtensions.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
using Microsoft.SyndicationFeed;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using TerribleDev.Blog.Web.Models;
|
||||
|
||||
namespace TerribleDev.Blog.Web
|
||||
{
|
||||
public static class IPostExtensions
|
||||
{
|
||||
public static SyndicationItem ToSyndicationItem(this IPost x)
|
||||
{
|
||||
return new SyndicationItem()
|
||||
{
|
||||
Title = x.Title,
|
||||
Description = x.Content.ToString(),
|
||||
Id = $"https://blog.terribledev.io/{x.Url}",
|
||||
Published = x.PublishDate
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
24
src/TerribleDev.Blog.Web/Extensions/StringExtension.cs
Normal file
24
src/TerribleDev.Blog.Web/Extensions/StringExtension.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace TerribleDev.Blog.Web
|
||||
{
|
||||
public static class StringExtension
|
||||
{
|
||||
public static string WithoutSpecialCharacters(this string str)
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
foreach (char c in str)
|
||||
{
|
||||
if ((c >= '0' && c <= '9') || (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || c == '_' || c == '-')
|
||||
{
|
||||
sb.Append(c);
|
||||
}
|
||||
}
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
73
src/TerribleDev.Blog.Web/Factories/BlogFactory.cs
Normal file
73
src/TerribleDev.Blog.Web/Factories/BlogFactory.cs
Normal file
@@ -0,0 +1,73 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.IO;
|
||||
using TerribleDev.Blog.Web.Models;
|
||||
using YamlDotNet.Serialization;
|
||||
using Microsoft.AspNetCore.Html;
|
||||
using Markdig;
|
||||
using TerribleDev.Blog.Web.MarkExtension.TerribleDev.Blog.Web.ExternalLinkParser;
|
||||
|
||||
namespace TerribleDev.Blog.Web
|
||||
{
|
||||
public class BlogFactory
|
||||
{
|
||||
public List<IPost> GetAllPosts()
|
||||
{
|
||||
// why didn't I use f# I'd have a pipe operator by now
|
||||
var posts = GetPosts();
|
||||
var postsAsText = posts.Select(GetFileText);
|
||||
return Task.WhenAll(postsAsText).Result.AsParallel().Select(b => ParsePost(b.text, b.fileInfo.Name)).ToList();
|
||||
}
|
||||
|
||||
private static async Task<(string text, FileInfo fileInfo)> GetFileText(string filePath)
|
||||
{
|
||||
var fileInfo = new FileInfo(filePath);
|
||||
var text = await File.ReadAllTextAsync(fileInfo.FullName);
|
||||
return (text, fileInfo);
|
||||
|
||||
}
|
||||
|
||||
public IEnumerable<string> GetPosts() => Directory.EnumerateFiles(Path.Combine(Directory.GetCurrentDirectory(), "Posts"), "*.md", SearchOption.TopDirectoryOnly);
|
||||
|
||||
public PostSettings ParseYaml(string ymlText)
|
||||
{
|
||||
var serializer = new DeserializerBuilder().Build();
|
||||
return serializer.Deserialize<PostSettings>(ymlText);
|
||||
|
||||
}
|
||||
public IPost ParsePost(string postText, string fileName)
|
||||
{
|
||||
var splitFile = postText.Split("---");
|
||||
var ymlRaw = splitFile[0];
|
||||
var markdownText = string.Join("", splitFile.Skip(1));
|
||||
List<string> postImages = new List<string>();
|
||||
var pipeline = new MarkdownPipelineBuilder()
|
||||
.Use<TargetLinkExtension>()
|
||||
.Use<ImageRecorder>(new ImageRecorder(ref postImages))
|
||||
.UseMediaLinks()
|
||||
.UseEmojiAndSmiley()
|
||||
.Build();
|
||||
var postContent = Markdown.ToHtml(markdownText, pipeline);
|
||||
var postContentPlain = String.Join("", Markdown.ToPlainText(markdownText, pipeline).Split("<!-- more -->"));
|
||||
var postSettings = ParseYaml(ymlRaw);
|
||||
var resolvedUrl = !string.IsNullOrWhiteSpace(postSettings.permalink) ? postSettings.permalink : fileName.Split('.')[0].Replace(' ', '-').WithoutSpecialCharacters();
|
||||
var summary = postContent.Split("<!-- more -->")[0];
|
||||
var postSummaryPlain = postContentPlain.Split("<!-- more -->")[0];
|
||||
return new Post()
|
||||
{
|
||||
PublishDate = postSettings.date.ToUniversalTime(),
|
||||
tags = postSettings.tags?.Select(a => a.Replace(' ', '-').WithoutSpecialCharacters().ToLower()).ToList() ?? new List<string>(),
|
||||
Title = postSettings.title,
|
||||
Url = resolvedUrl,
|
||||
Content = new HtmlString(postContent),
|
||||
Summary = new HtmlString(summary),
|
||||
SummaryPlain = postSummaryPlain,
|
||||
SummaryPlainShort = (postContentPlain.Length <= 147 ? postContentPlain : postContentPlain.Substring(0, 146)) + "...",
|
||||
ContentPlain = postContentPlain,
|
||||
Images = postImages.Distinct().Select(a => a.StartsWith('/') ? a : $"/{resolvedUrl}/{a}").ToList()
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
14
src/TerribleDev.Blog.Web/Factories/IBlogFactory.cs
Normal file
14
src/TerribleDev.Blog.Web/Factories/IBlogFactory.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using TerribleDev.Blog.Web.Models;
|
||||
|
||||
namespace TerribleDev.Blog.Web
|
||||
{
|
||||
public interface IBlogFactory
|
||||
{
|
||||
Task<IEnumerable<IPost>> GetAllPosts();
|
||||
IEnumerable<string> GetPosts();
|
||||
IPost ParsePost(string postText, string fileName);
|
||||
IPostSettings ParseYaml(string ymlText);
|
||||
}
|
||||
}
|
||||
71
src/TerribleDev.Blog.Web/MarkExtension/ExternalLinkParser.cs
Normal file
71
src/TerribleDev.Blog.Web/MarkExtension/ExternalLinkParser.cs
Normal file
@@ -0,0 +1,71 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Markdig.Syntax.Inlines;
|
||||
|
||||
namespace TerribleDev.Blog.Web.MarkExtension
|
||||
{
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Markdig;
|
||||
using Markdig.Renderers;
|
||||
using Markdig.Renderers.Html;
|
||||
using Markdig.Renderers.Html.Inlines;
|
||||
using Markdig.Syntax.Inlines;
|
||||
|
||||
namespace TerribleDev.Blog.Web.ExternalLinkParser
|
||||
{
|
||||
/// <summary>
|
||||
/// Extension for extending image Markdown links in case a video or an audio file is linked and output proper link.
|
||||
/// </summary>
|
||||
/// <seealso cref="Markdig.IMarkdownExtension" />
|
||||
public class TargetLinkExtension : IMarkdownExtension
|
||||
{
|
||||
|
||||
public void Setup(MarkdownPipelineBuilder pipeline)
|
||||
{
|
||||
}
|
||||
|
||||
public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer)
|
||||
{
|
||||
var htmlRenderer = renderer as HtmlRenderer;
|
||||
if (htmlRenderer != null)
|
||||
{
|
||||
var inlineRenderer = htmlRenderer.ObjectRenderers.FindExact<LinkInlineRenderer>();
|
||||
if (inlineRenderer != null)
|
||||
{
|
||||
inlineRenderer.TryWriters.Remove(TryLinkInlineRenderer);
|
||||
inlineRenderer.TryWriters.Add(TryLinkInlineRenderer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryLinkInlineRenderer(HtmlRenderer renderer, LinkInline linkInline)
|
||||
{
|
||||
if (linkInline.Url == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Uri uri;
|
||||
// Only process absolute Uri
|
||||
if (!Uri.TryCreate(linkInline.Url, UriKind.RelativeOrAbsolute, out uri) || !uri.IsAbsoluteUri)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
RenderTargetAttribute(uri, renderer, linkInline);
|
||||
return false;
|
||||
}
|
||||
|
||||
private void RenderTargetAttribute(Uri uri, HtmlRenderer renderer, LinkInline linkInline)
|
||||
{
|
||||
|
||||
linkInline.SetAttributes(new HtmlAttributes() { Properties = new List<KeyValuePair<string, string>>() { new KeyValuePair<string, string>("target", "_blank"), new KeyValuePair<string, string>("rel", "noopener"), } });
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
57
src/TerribleDev.Blog.Web/MarkExtension/ImageRecorder.cs
Normal file
57
src/TerribleDev.Blog.Web/MarkExtension/ImageRecorder.cs
Normal file
@@ -0,0 +1,57 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Markdig.Syntax.Inlines;
|
||||
|
||||
namespace TerribleDev.Blog.Web.MarkExtension
|
||||
{
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Markdig;
|
||||
using Markdig.Renderers;
|
||||
using Markdig.Renderers.Html;
|
||||
using Markdig.Renderers.Html.Inlines;
|
||||
using Markdig.Syntax.Inlines;
|
||||
|
||||
namespace TerribleDev.Blog.Web.ExternalLinkParser
|
||||
{
|
||||
public class ImageRecorder : IMarkdownExtension
|
||||
{
|
||||
private List<string> images = null;
|
||||
public ImageRecorder(ref List<string> images)
|
||||
{
|
||||
this.images = images;
|
||||
}
|
||||
public void Setup(MarkdownPipelineBuilder pipeline)
|
||||
{
|
||||
}
|
||||
|
||||
public void Setup(MarkdownPipeline pipeline, IMarkdownRenderer renderer)
|
||||
{
|
||||
var htmlRenderer = renderer as HtmlRenderer;
|
||||
if (htmlRenderer != null)
|
||||
{
|
||||
var inlineRenderer = htmlRenderer.ObjectRenderers.FindExact<LinkInlineRenderer>();
|
||||
if (inlineRenderer != null)
|
||||
{
|
||||
inlineRenderer.TryWriters.Remove(TryLinkInlineRenderer);
|
||||
inlineRenderer.TryWriters.Add(TryLinkInlineRenderer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryLinkInlineRenderer(HtmlRenderer renderer, LinkInline linkInline)
|
||||
{
|
||||
if (linkInline.Url == null || !linkInline.IsImage)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
this.images.Add(linkInline.Url);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
18
src/TerribleDev.Blog.Web/MarkExtension/PictureInline.cs
Normal file
18
src/TerribleDev.Blog.Web/MarkExtension/PictureInline.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
using Markdig.Renderers.Html;
|
||||
using Markdig.Syntax.Inlines;
|
||||
|
||||
namespace TerribleDev.Blog.Web.MarkExtension
|
||||
{
|
||||
public class PictureInlineRenderer : HtmlObjectRenderer<LinkInline>
|
||||
{
|
||||
protected override void Write(HtmlRenderer renderer, LinkInline link)
|
||||
{
|
||||
if(!link.IsImage)
|
||||
{
|
||||
base.Write(renderer, link);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
8
src/TerribleDev.Blog.Web/Models/BlogConfiguration.cs
Normal file
8
src/TerribleDev.Blog.Web/Models/BlogConfiguration.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
namespace TerribleDev.Blog.Web.Models
|
||||
{
|
||||
public class BlogConfiguration
|
||||
{
|
||||
public string Title { get; set; }
|
||||
public string Link { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@ namespace TerribleDev.Blog.Web.Models
|
||||
{
|
||||
public class GetTagViewModel
|
||||
{
|
||||
public IEnumerable<TerribleDev.Blog.Core.Models.Post> Posts { get; set; }
|
||||
public IEnumerable<IPost> Posts { get; set; }
|
||||
public string Tag { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ namespace TerribleDev.Blog.Web.Models
|
||||
{
|
||||
public class HomeViewModel
|
||||
{
|
||||
public IEnumerable<TerribleDev.Blog.Core.Models.Post> Posts { get; set;}
|
||||
public IEnumerable<IPost> Posts { get; set;}
|
||||
public int Page { get; set; }
|
||||
public string NextUrl { get; set; }
|
||||
public string PreviousUrl { get; set; }
|
||||
|
||||
23
src/TerribleDev.Blog.Web/Models/IPost.cs
Normal file
23
src/TerribleDev.Blog.Web/Models/IPost.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
using Microsoft.AspNetCore.Html;
|
||||
using YamlDotNet.Serialization;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace TerribleDev.Blog.Web.Models
|
||||
{
|
||||
public interface IPost
|
||||
{
|
||||
string Url { get; set; }
|
||||
string Title { get; set; }
|
||||
HtmlString Summary { get; set; }
|
||||
DateTime PublishDate { get; set; }
|
||||
HtmlString Content { get; set; }
|
||||
string ContentPlain { get; set; }
|
||||
string SummaryPlain { get; set; }
|
||||
string SummaryPlainShort { get; set; }
|
||||
IList<string> tags { get; set; }
|
||||
IList<string> Images { get; set;}
|
||||
}
|
||||
}
|
||||
16
src/TerribleDev.Blog.Web/Models/IPostSettings.cs
Normal file
16
src/TerribleDev.Blog.Web/Models/IPostSettings.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace TerribleDev.Blog.Web.Models
|
||||
{
|
||||
public interface IPostSettings
|
||||
{
|
||||
string id { get; set; }
|
||||
List<string> tags { get; set; }
|
||||
string title { get; set; }
|
||||
string permalink { get; set; }
|
||||
string thumbnailImage { get; set; }
|
||||
DateTimeOffset date { get; set; }
|
||||
DateTimeOffset updated { get; set; }
|
||||
}
|
||||
}
|
||||
22
src/TerribleDev.Blog.Web/Models/Post.cs
Normal file
22
src/TerribleDev.Blog.Web/Models/Post.cs
Normal file
@@ -0,0 +1,22 @@
|
||||
using Microsoft.AspNetCore.Html;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace TerribleDev.Blog.Web.Models
|
||||
{
|
||||
[DebuggerDisplay("{Title}")]
|
||||
public class Post : IPost
|
||||
{
|
||||
public string Url { get; set; }
|
||||
public string Title { get; set; }
|
||||
public DateTime PublishDate { get; set; }
|
||||
public HtmlString Content { get; set; }
|
||||
public HtmlString Summary { get; set; }
|
||||
public string ContentPlain { get; set; }
|
||||
public string SummaryPlain { get; set; }
|
||||
public string SummaryPlainShort { get; set; }
|
||||
public IList<string> tags { get; set; }
|
||||
public IList<string> Images { get; set;}
|
||||
}
|
||||
}
|
||||
9
src/TerribleDev.Blog.Web/Models/PostModel.cs
Normal file
9
src/TerribleDev.Blog.Web/Models/PostModel.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
using Microsoft.AspNetCore.Html;
|
||||
|
||||
namespace TerribleDev.Blog.Web.Models
|
||||
{
|
||||
public class PostModel
|
||||
{
|
||||
public HtmlString Content { get; set; }
|
||||
}
|
||||
}
|
||||
19
src/TerribleDev.Blog.Web/Models/PostSettings.cs
Normal file
19
src/TerribleDev.Blog.Web/Models/PostSettings.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace TerribleDev.Blog.Web.Models
|
||||
{
|
||||
public class PostSettings
|
||||
{
|
||||
public List<string> tags { get; set; }
|
||||
public string title { get; set; }
|
||||
public string permalink { get; set; }
|
||||
public DateTime date { get; set; }
|
||||
public DateTime updated { get; set; }
|
||||
public string id { get; set; }
|
||||
public string thumbnail_image { get; set; }
|
||||
public string thumbnailImage { get; set; }
|
||||
public string thumbnail_image_position { get; set; }
|
||||
public string layout { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -15,20 +15,33 @@ using Microsoft.Extensions.FileProviders;
|
||||
using Microsoft.Net.Http.Headers;
|
||||
using HardHat.Middlewares;
|
||||
using HardHat;
|
||||
using TerribleDev.Blog.Web.Models;
|
||||
|
||||
namespace TerribleDev.Blog.Web
|
||||
{
|
||||
public class Startup
|
||||
{
|
||||
public Startup(IConfiguration configuration)
|
||||
public Startup(IConfiguration configuration, IHostingEnvironment env)
|
||||
{
|
||||
Configuration = configuration;
|
||||
Env = env;
|
||||
}
|
||||
|
||||
public IConfiguration Configuration { get; }
|
||||
public IHostingEnvironment Env { get; }
|
||||
|
||||
// This method gets called by the runtime. Use this method to add services to the container.
|
||||
public void ConfigureServices(IServiceCollection services)
|
||||
{
|
||||
Func<BlogConfiguration> getBlog = () => Configuration.GetSection("Blog").Get<BlogConfiguration>();
|
||||
if (Env.IsDevelopment())
|
||||
{
|
||||
services.AddTransient(a => getBlog());
|
||||
}
|
||||
else
|
||||
{
|
||||
services.AddSingleton(getBlog());
|
||||
}
|
||||
services.AddResponseCompression(a =>
|
||||
{
|
||||
a.EnableForHttps = true;
|
||||
@@ -57,31 +70,47 @@ namespace TerribleDev.Blog.Web
|
||||
|
||||
app.UseHttpsRedirection();
|
||||
app.UseResponseCompression();
|
||||
|
||||
|
||||
var cacheTime = env.IsDevelopment() ? 0 : 31536000;
|
||||
app.UseStaticFiles(new StaticFileOptions
|
||||
{
|
||||
OnPrepareResponse = ctx =>
|
||||
{
|
||||
ctx.Context.Response.Headers[HeaderNames.CacheControl] =
|
||||
"public,max-age=" + cacheTime;
|
||||
}
|
||||
OnPrepareResponse = ctx =>
|
||||
{
|
||||
ctx.Context.Response.Headers[HeaderNames.CacheControl] =
|
||||
"public,max-age=" + cacheTime;
|
||||
}
|
||||
});
|
||||
app.UseStaticFiles(new StaticFileOptions
|
||||
{
|
||||
FileProvider = new PhysicalFileProvider(Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "img")),
|
||||
OnPrepareResponse = ctx =>
|
||||
{
|
||||
ctx.Context.Response.Headers[HeaderNames.CacheControl] =
|
||||
"public,max-age=" + cacheTime;
|
||||
}
|
||||
OnPrepareResponse = ctx =>
|
||||
{
|
||||
ctx.Context.Response.Headers[HeaderNames.CacheControl] =
|
||||
"public,max-age=" + cacheTime;
|
||||
}
|
||||
});
|
||||
app.UseRewriter(new Microsoft.AspNetCore.Rewrite.RewriteOptions().AddRedirect("(.*[^/|.xml|.html])$", "$1/", 301));
|
||||
app.UseIENoOpen();
|
||||
app.UseNoMimeSniff();
|
||||
app.UseCrossSiteScriptingFilters();
|
||||
app.UseFrameGuard(new FrameGuardOptions(FrameGuardOptions.FrameGuard.SAMEORIGIN));
|
||||
app.UseHsts(TimeSpan.FromDays(30), false, preload: true);
|
||||
app.UseHsts(TimeSpan.FromDays(365), false, preload: true);
|
||||
app.UseContentSecurityPolicy(
|
||||
new ContentSecurityPolicy()
|
||||
{
|
||||
// DefaultSrc = new HashSet<string>() {
|
||||
// CSPConstants.Self, "https://www.google-analytics.com", "https://www.googletagmanager.com", "https://stats.g.doubleclick.net"
|
||||
// },
|
||||
// ScriptSrc = new HashSet<string>()
|
||||
// {
|
||||
// CSPConstants.Self, CSPConstants.UnsafeInline, "https://www.google-analytics.com", "https://www.googletagmanager.com", "https://stats.g.doubleclick.net"
|
||||
// },
|
||||
// StyleSrc = new HashSet<string>()
|
||||
// {
|
||||
// CSPConstants.Self, CSPConstants.UnsafeInline
|
||||
// },
|
||||
UpgradeInsecureRequests = true
|
||||
});
|
||||
app.UseOutputCaching();
|
||||
app.UseMvc();
|
||||
}
|
||||
|
||||
72
src/TerribleDev.Blog.Web/Taghelpers/InlineCss.cs
Normal file
72
src/TerribleDev.Blog.Web/Taghelpers/InlineCss.cs
Normal file
@@ -0,0 +1,72 @@
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Razor.TagHelpers;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Microsoft.Extensions.FileProviders;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace TerribleDev.Blog.Web.Taghelpers
|
||||
{
|
||||
[HtmlTargetElement("inline-style")]
|
||||
public class InlineStyleTagHelper : TagHelper
|
||||
{
|
||||
[HtmlAttributeName("href")]
|
||||
public string Href { get; set; }
|
||||
|
||||
private IHostingEnvironment HostingEnvironment { get; }
|
||||
private IMemoryCache Cache { get; }
|
||||
|
||||
|
||||
|
||||
public InlineStyleTagHelper(IHostingEnvironment hostingEnvironment, IMemoryCache cache)
|
||||
{
|
||||
HostingEnvironment = hostingEnvironment;
|
||||
Cache = cache;
|
||||
}
|
||||
|
||||
|
||||
public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
|
||||
{
|
||||
var path = Href;
|
||||
|
||||
// Get the value from the cache, or compute the value and add it to the cache
|
||||
var fileContent = await Cache.GetOrCreateAsync("InlineStyleTagHelper-" + path, async entry =>
|
||||
{
|
||||
var fileProvider = HostingEnvironment.WebRootFileProvider;
|
||||
if(HostingEnvironment.IsDevelopment())
|
||||
{
|
||||
var changeToken = fileProvider.Watch(path);
|
||||
entry.AddExpirationToken(changeToken);
|
||||
}
|
||||
|
||||
entry.SetPriority(CacheItemPriority.NeverRemove);
|
||||
|
||||
var file = fileProvider.GetFileInfo(path);
|
||||
if (file == null || !file.Exists)
|
||||
return null;
|
||||
|
||||
return await ReadFileContent(file);
|
||||
});
|
||||
|
||||
if (fileContent == null)
|
||||
{
|
||||
output.SuppressOutput();
|
||||
return;
|
||||
}
|
||||
|
||||
output.TagName = "style";
|
||||
output.Attributes.RemoveAll("href");
|
||||
output.Content.AppendHtml(fileContent);
|
||||
}
|
||||
|
||||
private static async Task<string> ReadFileContent(IFileInfo file)
|
||||
{
|
||||
using (var stream = file.CreateReadStream())
|
||||
using (var textReader = new StreamReader(stream))
|
||||
{
|
||||
return await textReader.ReadToEndAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
41
src/TerribleDev.Blog.Web/Taghelpers/Mobile.cs
Normal file
41
src/TerribleDev.Blog.Web/Taghelpers/Mobile.cs
Normal file
@@ -0,0 +1,41 @@
|
||||
using Microsoft.AspNetCore.Razor.TagHelpers;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace TerribleDev.Blog.Web.Taghelpers
|
||||
{
|
||||
[HtmlTargetElement("desktopOnly", TagStructure = TagStructure.NormalOrSelfClosing)]
|
||||
public class DesktopTagHelper : TagHelper
|
||||
{
|
||||
static Regex MobileCheck = new Regex(@"(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino", RegexOptions.IgnoreCase | RegexOptions.Multiline | RegexOptions.Compiled);
|
||||
static ConcurrentDictionary<string, bool> CachedChecks = new ConcurrentDictionary<string, bool>();
|
||||
public string UserAgent { get; set; }
|
||||
public override void Process(TagHelperContext context, TagHelperOutput output)
|
||||
{
|
||||
output.TagName = null;
|
||||
if (string.IsNullOrEmpty(UserAgent))
|
||||
{
|
||||
return;
|
||||
}
|
||||
var shouldRender = true;
|
||||
if(CachedChecks.TryGetValue(UserAgent, out var cacheResult))
|
||||
{
|
||||
shouldRender = cacheResult;
|
||||
}
|
||||
else
|
||||
{
|
||||
var isMobile = MobileCheck.IsMatch(UserAgent);
|
||||
shouldRender = !isMobile;
|
||||
CachedChecks.TryAdd(UserAgent, !isMobile);
|
||||
}
|
||||
if(!shouldRender)
|
||||
{
|
||||
output.SuppressOutput();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -26,7 +26,7 @@
|
||||
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.0.2105168" />
|
||||
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="2.2.0" />
|
||||
<PackageReference Include="YamlDotNet" Version="5.3.0" />
|
||||
<PackageReference Include="HardHat" Version="2.0.0" />
|
||||
<PackageReference Include="HardHat" Version="2.1.1" />
|
||||
<PackageReference Include="Microsoft.SyndicationFeed.ReaderWriter" Version="1.0.2" />
|
||||
<PackageReference Include="WebEssentials.AspNetCore.OutputCaching" Version="1.0.16" />
|
||||
</ItemGroup>
|
||||
@@ -36,12 +36,4 @@
|
||||
<Content Include="Posts\*.md" CopyToOutputDirectory="Always" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\TerribleDev.Blog.MarkdownPlugins\TerribleDev.Blog.MarkdownPlugins.csproj" />
|
||||
<ProjectReference Include="..\TerribleDev.Blog.Core\TerribleDev.Blog.Core.fsproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Remove="Factories\BlogFactory.cs" />
|
||||
<Compile Remove="Extensions\IPostExtensions.cs" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
@model TerribleDev.Blog.Core.Models.Post
|
||||
@inject BlogConfiguration config
|
||||
@model IPost
|
||||
@{
|
||||
ViewData["Title"] = "Post";
|
||||
ViewData["HideNav"] = true;
|
||||
@@ -13,7 +14,7 @@
|
||||
<meta property="og:type" content="blog">
|
||||
<meta property="og:title" content="@Model.Title">
|
||||
<meta property="og:url" content="https://blog.terribledev.io/@Model.Url/">
|
||||
<meta property="og:site_name" content="The Ramblings of TerribleDev">
|
||||
<meta property="og:site_name" content="@config.Title">
|
||||
<meta property="og:description" content="@Model.SummaryPlainShort">
|
||||
<meta property="og:updated_time" content="@Model.PublishDate.ToString("O")">
|
||||
<meta name="twitter:card" content="summary">
|
||||
@@ -23,10 +24,10 @@
|
||||
<meta property="og:image" content="https://www.gravatar.com/avatar/333e3cea32cd17ff2007d131df336061?s=640" />
|
||||
@foreach(var image in Model.Images.Take(6))
|
||||
{
|
||||
<meta property="og:image" content="https://blog.terribledev.io@(image)">
|
||||
<meta property="og:image" content="https://blog.terribledev.io@(image)">
|
||||
}
|
||||
@if(Model.Images.Length > 0)
|
||||
@if(Model.Images.Count > 0)
|
||||
{
|
||||
<meta name="twitter:image" content="https://blog.terribledev.io@(Model.Images[0])">
|
||||
<meta name="twitter:image" content="https://blog.terribledev.io@(Model.Images[0])">
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
@model TerribleDev.Blog.Core.Models.Post
|
||||
@model IPost
|
||||
|
||||
<article itemprop="blogPost">
|
||||
<h1 itemprop="headline" class="headline">@Model.Title</h1>
|
||||
<time class="headlineSubtext" itemprop="datePublished" content="@Model.PublishDate.ToString()">@Model.PublishDate.ToString("D")</time>
|
||||
@Model.Content
|
||||
@if (Model.tags.Length > 0)
|
||||
@if (Model.tags.Count > 0)
|
||||
{
|
||||
<div>
|
||||
<span>Tagged In:</span><br />
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@model TerribleDev.Blog.Core.Models.Post
|
||||
@model IPost
|
||||
|
||||
<article class="btmRule">
|
||||
<h3 itemprop="headline" class="headline"><a href="/@Model.Url/" class="link-unstyled">@Model.Title</a></h3>
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
<meta name="description" content="My name is Tommy Parnell. I usually go by TerribleDev on the internets. These are just some of my writings and rants about the software space." />
|
||||
@inject BlogConfiguration config
|
||||
<meta name="description" content="My name is Tommy Parnell. I usually go by TerribleDev on the internets. These are just some of my writings and rants about the software space." />
|
||||
<meta property="og:type" content="blog">
|
||||
<meta property="og:title" content="The Ramblings of TerribleDev">
|
||||
<meta property="og:title" content="@config.Title">
|
||||
<meta property="og:url" content="https://blog.terribledev.io/">
|
||||
<meta property="og:site_name" content="The Ramblings of TerribleDev">
|
||||
<meta property="og:site_name" content="@config.Title">
|
||||
<meta property="og:description" content="My name is Tommy Parnell. I usually go by TerribleDev on the internets. These are just some of my writings and rants about the software space.">
|
||||
<meta name="twitter:card" content="summary">
|
||||
<meta name="twitter:title" content="The Ramblings of TerribleDev">
|
||||
<meta name="twitter:title" content="@config.Title">
|
||||
<meta name="twitter:description" content="My name is Tommy Parnell. I usually go by TerribleDev on the internets. These are just some of my writings and rants about the software space.">
|
||||
<meta name="twitter:creator" content="@@TerribleDev">
|
||||
<meta property="og:image" content="https://www.gravatar.com/avatar/333e3cea32cd17ff2007d131df336061?s=640" />
|
||||
@@ -1,20 +1,24 @@
|
||||
<!DOCTYPE html>
|
||||
@inject BlogConfiguration config
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<partial name="Gtm" />
|
||||
<environment names="Production">
|
||||
<partial name="Gtm" />
|
||||
</environment>
|
||||
<meta name="author" content="Tommy "TerribleDev" Parnell" />
|
||||
<meta name="theme-color" content="#4A4A4A" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="alternate" type="application/atom+xml" async title="RSS" href="/rss.xml">
|
||||
<link rel="manifest" href="~/manifest.json" async asp-append-version="true">
|
||||
<link asp-append-version="true" rel="icon" async href="~/favicon.ico" />
|
||||
<title>@ViewData["Title"] - The Ramblings of TerribleDev</title>
|
||||
<title>@ViewData["Title"] - @config.Title</title>
|
||||
<environment names="Development">
|
||||
<link asp-append-version="true" rel="stylesheet" href="~/css/site.css" />
|
||||
<inline-style href="css/site.css"></inline-style>
|
||||
</environment>
|
||||
<environment names="Production">
|
||||
<link asp-append-version="true" rel="stylesheet" href="~/css/site.min.css" />
|
||||
<inline-style href="css/site.min.css"></inline-style>
|
||||
</environment>
|
||||
@RenderSection("Head", false)
|
||||
</head>
|
||||
@@ -22,7 +26,7 @@
|
||||
<partial name="Nav" />
|
||||
<header class="header">
|
||||
<svg aria-label="Open Menu" id="menuBtn" role="button" xmlns="http://www.w3.org/2000/svg" width="32" height="32"><path d="M4 10h24c1.104 0 2-.896 2-2s-.896-2-2-2H4c-1.104 0-2 .896-2 2s.896 2 2 2zm24 4H4c-1.104 0-2 .896-2 2s.896 2 2 2h24c1.104 0 2-.896 2-2s-.896-2-2-2zm0 8H4c-1.104 0-2 .896-2 2s.896 2 2 2h24c1.104 0 2-.896 2-2s-.896-2-2-2z" /></svg>
|
||||
<div class="headerCallout"><a href="/" class="link-unstyled ">The Ramblings of TerribleDev</a></div>
|
||||
<div class="headerCallout"><a href="/" class="link-unstyled ">@config.Title</a></div>
|
||||
</header>
|
||||
@{
|
||||
var bodyBump = ViewData["HideNav"] == null ? "bodyWithNav": "";
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
@model Dictionary<string, List<TerribleDev.Blog.Core.Models.Post>>
|
||||
@model Dictionary<string, List<IPost>>
|
||||
@{
|
||||
ViewData["Title"] = "all-tags";
|
||||
}
|
||||
<h2>All Tags</h2>
|
||||
<cache>
|
||||
@foreach (var tag in Model.Keys)
|
||||
{
|
||||
<a href="/tag/@tag/" class="btn block">@tag</a>
|
||||
}
|
||||
@foreach (var tag in Model.Keys)
|
||||
{
|
||||
<a href="/tag/@tag/" class="btn block">@tag</a>
|
||||
}
|
||||
</cache>
|
||||
@section Head {
|
||||
<partial name="StockMeta" />
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
@using TerribleDev.Blog.Web
|
||||
@using TerribleDev.Blog.Web.Models
|
||||
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
@addTagHelper *, TerribleDev.Blog.Web
|
||||
@@ -9,5 +9,9 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
"AllowedHosts": "*",
|
||||
"Blog": {
|
||||
"title": "The Ramblings of TerribleDev",
|
||||
"link": "https://blog.terribledev.io"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user