7 Commits

Author SHA1 Message Date
Tommy Parnell
d8a0004bc5 commit to fs in core only 2019-01-30 21:58:53 -05:00
tparnell
b7486dcc85 the delete-ening 2019-01-30 09:27:28 -05:00
tparnell
ab9c9fdc90 it works in fsharp 2019-01-30 08:36:38 -05:00
Tommy Parnell
60fe3d8c2a stop 2019-01-30 00:58:47 -05:00
Tommy Parnell
115580f0f4 stop for now 2019-01-30 00:51:01 -05:00
Tommy Parnell
c9229018db stop 2019-01-30 00:20:44 -05:00
tparnell
03eeef20d7 stop 2019-01-29 22:01:48 -05:00
36 changed files with 420 additions and 416 deletions

View File

@@ -0,0 +1,34 @@
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();
}
}
}

View File

@@ -0,0 +1,62 @@
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"), } });
}
}
}

View File

@@ -0,0 +1,48 @@
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;
}
}
}

View File

@@ -0,0 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Markdig" Version="0.15.7" />
</ItemGroup>
</Project>

View File

@@ -7,6 +7,10 @@ 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
@@ -29,12 +33,38 @@ 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}

View File

@@ -0,0 +1,85 @@
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

View File

@@ -0,0 +1,17 @@
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>
}

View File

@@ -0,0 +1,18 @@
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
}

View File

@@ -0,0 +1,9 @@
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)

View File

@@ -0,0 +1,23 @@
<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>

View File

@@ -0,0 +1,35 @@
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
}

View File

@@ -0,0 +1,8 @@
module Constants
module Constants=
let MORE = "<!-- more -->"
let YMLDIVIDER = "---"
type Tokens =
| MORE
| YMLDIVIDER

View File

@@ -0,0 +1,10 @@
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)
}

View File

@@ -12,10 +12,10 @@ namespace TerribleDev.Blog.Web.Controllers
{
public class HomeController : Controller
{
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)
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)
.Aggregate(
new Dictionary<string, List<IPost>>(),
new Dictionary<string, List<TerribleDev.Blog.Core.Models.Post>>(),
(accum, item) => {
foreach(var tag in item.tags)
{
@@ -25,19 +25,19 @@ namespace TerribleDev.Blog.Web.Controllers
}
else
{
accum[tag] = new List<IPost>() { item };
accum[tag] = new List<TerribleDev.Blog.Core.Models.Post>() { item };
}
}
return accum;
});
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) =>
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) =>
{
var highestPage = accum.Keys.Max();
var current = accum[highestPage].Count;
if (current >= 10)
{
accum[highestPage + 1] = new List<IPost>() { item };
accum[highestPage + 1] = new List<TerribleDev.Blog.Core.Models.Post>() { item };
return accum;
}
accum[highestPage].Add(item);

View File

@@ -16,7 +16,7 @@ namespace TerribleDev.Blog.Web.Controllers
public class SeoController : Controller
{
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(a => a.ToSyndicationItem()).ToList();
public static IEnumerable<SyndicationItem> postsToSyndication = HomeController.postsAsList.Select(Core.Models.Util.ToSyndicationItem).ToList();
[Route("/rss")]
[Route("/rss.xml")]
[ResponseCache(Duration = 7200)]

View File

@@ -1,23 +0,0 @@
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
};
}
}
}

View File

@@ -1,24 +0,0 @@
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();
}
}
}

View File

@@ -1,73 +0,0 @@
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()
};
}
}
}

View File

@@ -1,14 +0,0 @@
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);
}
}

View File

@@ -1,71 +0,0 @@
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"), } });
}
}
}
}

View File

@@ -1,57 +0,0 @@
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 false;
}
}
}
}

View File

@@ -7,7 +7,7 @@ namespace TerribleDev.Blog.Web.Models
{
public class GetTagViewModel
{
public IEnumerable<IPost> Posts { get; set; }
public IEnumerable<TerribleDev.Blog.Core.Models.Post> Posts { get; set; }
public string Tag { get; set; }
}
}

View File

@@ -4,7 +4,7 @@ namespace TerribleDev.Blog.Web.Models
{
public class HomeViewModel
{
public IEnumerable<IPost> Posts { get; set;}
public IEnumerable<TerribleDev.Blog.Core.Models.Post> Posts { get; set;}
public int Page { get; set; }
public string NextUrl { get; set; }
public string PreviousUrl { get; set; }

View File

@@ -1,23 +0,0 @@
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;}
}
}

View File

@@ -1,16 +0,0 @@
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; }
}
}

View File

@@ -1,22 +0,0 @@
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;}
}
}

View File

@@ -1,9 +0,0 @@
using Microsoft.AspNetCore.Html;
namespace TerribleDev.Blog.Web.Models
{
public class PostModel
{
public HtmlString Content { get; set; }
}
}

View File

@@ -1,19 +0,0 @@
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; }
}
}

View File

@@ -1,41 +0,0 @@
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();
}
}
}
}

View File

@@ -36,4 +36,12 @@
<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>

View File

@@ -1,4 +1,4 @@
@model IPost
@model TerribleDev.Blog.Core.Models.Post
@{
ViewData["Title"] = "Post";
ViewData["HideNav"] = true;
@@ -23,10 +23,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.Count > 0)
@if(Model.Images.Length > 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])">
}
}

View File

@@ -1,10 +1,10 @@
@model IPost
@model TerribleDev.Blog.Core.Models.Post
<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.Count > 0)
@if (Model.tags.Length > 0)
{
<div>
<span>Tagged In:</span><br />

View File

@@ -1,4 +1,4 @@
@model IPost
@model TerribleDev.Blog.Core.Models.Post
<article class="btmRule">
<h3 itemprop="headline" class="headline"><a href="/@Model.Url/" class="link-unstyled">@Model.Title</a></h3>

View File

@@ -1,13 +1,13 @@
@model Dictionary<string, List<IPost>>
@model Dictionary<string, List<TerribleDev.Blog.Core.Models.Post>>
@{
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" />

View File

@@ -1,4 +1,3 @@
@using TerribleDev.Blog.Web
@using TerribleDev.Blog.Web.Models
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@addTagHelper *, TerribleDev.Blog.Web
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers