├── src └── Bloggy │ ├── Pages │ ├── Account │ │ ├── Logout.cshtml │ │ ├── Logout.cshtml.cs │ │ ├── Login.cshtml │ │ ├── Posts │ │ │ ├── Create.cshtml.cs │ │ │ ├── Edit.cshtml.cs │ │ │ ├── Create.cshtml │ │ │ └── Edit.cshtml │ │ └── Login.cshtml.cs │ ├── _ViewStart.cshtml │ ├── _ViewImports.cshtml │ ├── Error.cshtml │ ├── _LoginPartial.cshtml │ ├── Contact.cshtml │ ├── Archive.cshtml │ ├── Tags.cshtml │ ├── About.cshtml │ ├── Index.cshtml │ ├── Archive.cshtml.cs │ ├── Index.cshtml.cs │ ├── _CommentFormPartial.cshtml │ ├── Tags.cshtml.cs │ ├── Post.cshtml │ ├── Post.cshtml.cs │ └── Themes │ │ ├── Dark │ │ ├── _Layout.cshtml │ │ └── styles │ │ │ └── bloggy.css │ │ └── Light │ │ ├── _Layout.cshtml │ │ └── styles │ │ └── bloggy.css │ ├── wwwroot │ ├── images │ │ └── logo.png │ ├── fonts │ │ ├── Roboto-Black.ttf │ │ ├── Roboto-Bold.ttf │ │ ├── Roboto-Light.ttf │ │ ├── Roboto-Thin.ttf │ │ ├── Roboto-Italic.ttf │ │ ├── Roboto-Medium.ttf │ │ ├── Roboto-Regular.ttf │ │ ├── Roboto-BoldItalic.ttf │ │ ├── Roboto-ThinItalic.ttf │ │ ├── Roboto-BlackItalic.ttf │ │ ├── Roboto-LightItalic.ttf │ │ └── Roboto-MediumItalic.ttf │ ├── scripts │ │ ├── codemirror │ │ │ └── tablist.js │ │ └── simplemde.js │ └── styles │ │ ├── simplemde.css │ │ └── simplemde.min.css │ ├── Models │ ├── PasswordFormat.cs │ ├── Credential.cs │ ├── User.cs │ ├── BloggingContext.cs │ ├── Blog.cs │ ├── Post.cs │ └── Comment.cs │ ├── TagHelpers │ ├── AlertType.cs │ ├── AlertTagHelper.cs │ ├── PartialTagHelper.cs │ ├── GravatarTagHelper.cs │ └── MarkdownTagHelper.cs │ ├── AppSettings.cs │ ├── Program.cs │ ├── web.config │ ├── Properties │ └── launchSettings.json │ ├── appsettings.json │ ├── Bloggy.csproj │ ├── ThemeViewLocationExpander.cs │ ├── Migrations │ ├── BloggingContextModelSnapshot.cs │ ├── 20161227000614_Initial.Designer.cs │ └── 20161227000614_Initial.cs │ └── Startup.cs ├── .gitignore ├── Bloggy.sln ├── README.md └── .gitattributes /src/Bloggy/Pages/Account/Logout.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | 3 | @model LogoutModel -------------------------------------------------------------------------------- /src/Bloggy/Pages/_ViewStart.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | Layout = "_Layout"; 3 | } 4 | -------------------------------------------------------------------------------- /src/Bloggy/wwwroot/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hishamco/Bloggy/HEAD/src/Bloggy/wwwroot/images/logo.png -------------------------------------------------------------------------------- /src/Bloggy/wwwroot/fonts/Roboto-Black.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hishamco/Bloggy/HEAD/src/Bloggy/wwwroot/fonts/Roboto-Black.ttf -------------------------------------------------------------------------------- /src/Bloggy/wwwroot/fonts/Roboto-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hishamco/Bloggy/HEAD/src/Bloggy/wwwroot/fonts/Roboto-Bold.ttf -------------------------------------------------------------------------------- /src/Bloggy/wwwroot/fonts/Roboto-Light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hishamco/Bloggy/HEAD/src/Bloggy/wwwroot/fonts/Roboto-Light.ttf -------------------------------------------------------------------------------- /src/Bloggy/wwwroot/fonts/Roboto-Thin.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hishamco/Bloggy/HEAD/src/Bloggy/wwwroot/fonts/Roboto-Thin.ttf -------------------------------------------------------------------------------- /src/Bloggy/wwwroot/fonts/Roboto-Italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hishamco/Bloggy/HEAD/src/Bloggy/wwwroot/fonts/Roboto-Italic.ttf -------------------------------------------------------------------------------- /src/Bloggy/wwwroot/fonts/Roboto-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hishamco/Bloggy/HEAD/src/Bloggy/wwwroot/fonts/Roboto-Medium.ttf -------------------------------------------------------------------------------- /src/Bloggy/wwwroot/fonts/Roboto-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hishamco/Bloggy/HEAD/src/Bloggy/wwwroot/fonts/Roboto-Regular.ttf -------------------------------------------------------------------------------- /src/Bloggy/wwwroot/fonts/Roboto-BoldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hishamco/Bloggy/HEAD/src/Bloggy/wwwroot/fonts/Roboto-BoldItalic.ttf -------------------------------------------------------------------------------- /src/Bloggy/wwwroot/fonts/Roboto-ThinItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hishamco/Bloggy/HEAD/src/Bloggy/wwwroot/fonts/Roboto-ThinItalic.ttf -------------------------------------------------------------------------------- /src/Bloggy/Pages/_ViewImports.cshtml: -------------------------------------------------------------------------------- 1 | @namespace Bloggy.Pages 2 | @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers 3 | @addTagHelper *, Bloggy -------------------------------------------------------------------------------- /src/Bloggy/wwwroot/fonts/Roboto-BlackItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hishamco/Bloggy/HEAD/src/Bloggy/wwwroot/fonts/Roboto-BlackItalic.ttf -------------------------------------------------------------------------------- /src/Bloggy/wwwroot/fonts/Roboto-LightItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hishamco/Bloggy/HEAD/src/Bloggy/wwwroot/fonts/Roboto-LightItalic.ttf -------------------------------------------------------------------------------- /src/Bloggy/wwwroot/fonts/Roboto-MediumItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hishamco/Bloggy/HEAD/src/Bloggy/wwwroot/fonts/Roboto-MediumItalic.ttf -------------------------------------------------------------------------------- /src/Bloggy/Models/PasswordFormat.cs: -------------------------------------------------------------------------------- 1 | namespace Bloggy.Models 2 | { 3 | public enum PasswordFormat 4 | { 5 | Clear, 6 | MD5, 7 | SHA1, 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/Bloggy/TagHelpers/AlertType.cs: -------------------------------------------------------------------------------- 1 | namespace Bloggy.TagHelpers 2 | { 3 | public enum AlertType 4 | { 5 | Error, 6 | Information, 7 | Success, 8 | Warning 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/Bloggy/Models/Credential.cs: -------------------------------------------------------------------------------- 1 | namespace Bloggy.Models 2 | { 3 | public class Credential 4 | { 5 | public PasswordFormat PasswordFormat { get; set; } 6 | 7 | public User User { get; set; } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/Bloggy/Pages/Error.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | 3 | @{ 4 | ViewBag.Title = "Error"; 5 | } 6 | 7 |
8 |

@ViewBag.Title

9 |

An error occurred while processing your request.

10 |
-------------------------------------------------------------------------------- /src/Bloggy/AppSettings.cs: -------------------------------------------------------------------------------- 1 | using Bloggy.Models; 2 | 3 | namespace Bloggy 4 | { 5 | public class AppSettings 6 | { 7 | public Blog Blog { get; set; } 8 | 9 | public Credential Credential { get; set; } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bin 2 | obj 3 | *.suo 4 | *.user 5 | _ReSharper.* 6 | *.DS_Store 7 | *.userprefs 8 | *.pidb 9 | *.vspx 10 | *.psess 11 | packages 12 | target 13 | artifacts 14 | StyleCop.Cache 15 | node_modules 16 | *.snk 17 | .nuget/NuGet.exe 18 | project.lock.json -------------------------------------------------------------------------------- /src/Bloggy/Models/User.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace Bloggy.Models 4 | { 5 | public class User 6 | { 7 | [Required] 8 | public string Name { get; set; } 9 | 10 | [Required] 11 | [DataType(DataType.Password)] 12 | public string Password { get; set; } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/Bloggy/Pages/_LoginPartial.cshtml: -------------------------------------------------------------------------------- 1 | @using System.Security.Principal 2 | 3 | @if (User.Identity.IsAuthenticated) 4 | { 5 |

[ @User.Identity.Name | Log Out ]

6 | } 7 | 8 | 9 | @if (!User.Identity.IsAuthenticated) 10 | { 11 | Log In 12 | } 13 | -------------------------------------------------------------------------------- /src/Bloggy/Models/BloggingContext.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | 3 | namespace Bloggy.Models 4 | { 5 | public class BloggingContext : DbContext 6 | { 7 | public BloggingContext(DbContextOptions options) 8 | : base(options) 9 | { 10 | 11 | } 12 | 13 | public DbSet Posts { get; set; } 14 | 15 | public DbSet Comments { get; set; } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Bloggy/Models/Blog.cs: -------------------------------------------------------------------------------- 1 | namespace Bloggy.Models 2 | { 3 | public class Blog 4 | { 5 | public string Name { get; set; } 6 | 7 | public string Description { get; set; } 8 | 9 | public string Image { get; set; } 10 | 11 | public int PostsPerPage { get; set; } 12 | 13 | public bool AllowComments { get; set; } 14 | 15 | public int DaysToComment { get; set; } 16 | 17 | public string Theme { get; set; } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Bloggy/Program.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Hosting; 2 | using System.IO; 3 | 4 | namespace Bloggy 5 | { 6 | public class Program 7 | { 8 | public static void Main(string[] args) 9 | { 10 | var host = new WebHostBuilder() 11 | .UseKestrel() 12 | .UseContentRoot(Directory.GetCurrentDirectory()) 13 | .UseIISIntegration() 14 | .UseStartup() 15 | .Build(); 16 | 17 | host.Run(); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Bloggy/Pages/Contact.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | 3 | @{ 4 | ViewBag.Title = "Contact"; 5 | } 6 | 7 |
8 |

@ViewBag.Title

9 |

Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book.

10 |

Twitter: @@bloggy

11 |

Google+: +bloggy

12 |

LinkedIn: bloggy

13 |
-------------------------------------------------------------------------------- /src/Bloggy/web.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/Bloggy/Pages/Account/Logout.cshtml.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Authentication; 2 | using Microsoft.AspNetCore.Authentication.Cookies; 3 | using Microsoft.AspNetCore.Mvc; 4 | using Microsoft.AspNetCore.Mvc.RazorPages; 5 | using System.Threading.Tasks; 6 | 7 | namespace Bloggy.Pages.Account 8 | { 9 | public class LogoutModel : PageModel 10 | { 11 | public async Task OnGet(string returnUrl = null) 12 | { 13 | await PageContext.HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); 14 | return RedirectToPage("/Index"); 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /src/Bloggy/Pages/Archive.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | 3 | @model ArchiveModel 4 | 5 | @{ 6 | ViewBag.Title = "Archive"; 7 | } 8 | 9 |
10 | @foreach (var group in Model.GroupedPostsByMonth) 11 | { 12 |
13 |

@group.Item1

14 |
    15 | @foreach (var post in group.Item2) 16 | { 17 |
  • @post.Title @if (User.Identity.IsAuthenticated){[ Edit ]}
  • 18 | } 19 |
20 |
21 | } 22 |
-------------------------------------------------------------------------------- /src/Bloggy/Pages/Tags.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | 3 | @model TagsModel 4 | 5 | @{ 6 | ViewBag.Title = "Tags"; 7 | } 8 | 9 |
10 |
11 | @foreach (var tag in Model.Tags) 12 | { 13 | @tag 14 | } 15 |
16 | 17 | @foreach (var tag in Model.Tags) 18 | { 19 |
20 |

@tag

21 |
    22 | @foreach (var post in Model.GetPosts(tag)) 23 | { 24 |
  • @post.Title
  • 25 | } 26 |
27 |
28 | } 29 |
-------------------------------------------------------------------------------- /src/Bloggy/Pages/About.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | 3 | @{ 4 | ViewBag.Title = "About"; 5 | } 6 | 7 |
8 |

@ViewBag.Title

9 |

Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.

10 |
-------------------------------------------------------------------------------- /src/Bloggy/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:8739/", 7 | "sslPort": 0 8 | } 9 | }, 10 | "profiles": { 11 | "IIS Express": { 12 | "commandName": "IISExpress", 13 | "launchBrowser": true, 14 | "environmentVariables": { 15 | "ASPNETCORE_ENVIRONMENT": "Development" 16 | } 17 | }, 18 | "Bloggy": { 19 | "commandName": "Project", 20 | "launchBrowser": true, 21 | "launchUrl": "http://localhost:5000", 22 | "environmentVariables": { 23 | "ASPNETCORE_ENVIRONMENT": "Development" 24 | } 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /src/Bloggy/Pages/Index.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | 3 | @model IndexModel 4 | 5 | @{ 6 | ViewBag.Title = "Home"; 7 | } 8 | 9 |
10 | @foreach (var post in Model.Posts) 11 | { 12 |
13 |

@post.Title

14 | 15 |

@post.Excerpt

16 | @if (User.Identity.IsAuthenticated) 17 | { 18 | [ Edit ] 19 | } 20 |
21 | } 22 |
23 |
24 | [ Create New Post ] 25 |
-------------------------------------------------------------------------------- /src/Bloggy/Models/Post.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel.DataAnnotations; 4 | 5 | namespace Bloggy.Models 6 | { 7 | public class Post 8 | { 9 | public int Id { get; set; } 10 | 11 | [Required] 12 | public string Title { get; set; } 13 | 14 | [Required] 15 | public string Slug { get; set; } 16 | 17 | public string Excerpt { get; set; } 18 | 19 | [Required] 20 | public string Content { get; set; } 21 | 22 | public DateTime LastModified { get; set; } 23 | 24 | public bool IsPublished { get; set; } 25 | 26 | public DateTime PublishedAt { get; set; } 27 | 28 | [Required] 29 | public string Tags { get; set; } 30 | 31 | public virtual ICollection Comments { get; set; } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Bloggy/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "AppSettings": { 3 | "Blog": { 4 | "Name": "bloggy", 5 | "Description": "A simple ASP.NET Core blog engine", 6 | "Image": "images/logo.png", 7 | "PostsPerPage": "5", 8 | "AllowComments": true, 9 | "DaysToComment": 7, 10 | "Theme": "Light" 11 | }, 12 | "Credential": { 13 | "PasswordFormat": "SHA1", 14 | "User": { 15 | "Name": "hishamco", 16 | "Password": "21bd12dc183f740ee76f27b78eb39c8ad972a757" // P@ssw0rd 17 | } 18 | } 19 | }, 20 | "Data": { 21 | "DefaultConnection": { 22 | "ConnectionString": "Filename=Blogging.db" 23 | } 24 | }, 25 | "Logging": { 26 | "IncludeScopes": false, 27 | "LogLevel": { 28 | "Default": "Debug", 29 | "System": "Information", 30 | "Microsoft": "Information" 31 | } 32 | }, 33 | "ReCaptcha": { 34 | "SiteKey": "Your ReCaptcha Key" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Bloggy/Bloggy.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp2.0 5 | true 6 | Bloggy 7 | Exe 8 | Bloggy 9 | bloggy-77a2c865-d54e-49bd-a25e-37df2334c17e 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/Bloggy/Models/Comment.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel.DataAnnotations; 3 | using System.ComponentModel.DataAnnotations.Schema; 4 | 5 | namespace Bloggy.Models 6 | { 7 | public class Comment 8 | { 9 | public int Id { get; set; } 10 | 11 | [Required] 12 | [StringLength(100, MinimumLength = 2)] 13 | [Display(Name = "Name")] 14 | public string Author { get; set; } 15 | 16 | [Required] 17 | [DataType(DataType.EmailAddress)] 18 | public string Email { get; set; } 19 | 20 | [DataType(DataType.Url)] 21 | public string Website { get; set; } 22 | 23 | [Required(ErrorMessage = "The Comment field is required.")] 24 | [Display(Name = "Comment")] 25 | public string Content { get; set; } 26 | 27 | public DateTime PublishedAt { get; set; } 28 | 29 | public int PostId { get; set; } 30 | 31 | [ForeignKey("PostId")] 32 | public Post Post { get; set; } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Bloggy/Pages/Archive.cshtml.cs: -------------------------------------------------------------------------------- 1 | using Bloggy.Models; 2 | using Microsoft.AspNetCore.Mvc.RazorPages; 3 | using Microsoft.EntityFrameworkCore; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using System.Threading.Tasks; 8 | 9 | namespace Bloggy.Pages 10 | { 11 | public class ArchiveModel : PageModel 12 | { 13 | private readonly BloggingContext _db; 14 | 15 | public ArchiveModel(BloggingContext db) 16 | { 17 | _db = db; 18 | } 19 | 20 | public IList>> GroupedPostsByMonth { get; private set; } 21 | 22 | public async Task OnGetAsync() 23 | { 24 | var notPublished = DateTime.MinValue.ToString("MMMM yyyy"); 25 | 26 | 27 | GroupedPostsByMonth = await _db.Posts.GroupBy(p => p.PublishedAt.ToString("MMMM yyyy"), 28 | (k, g) => Tuple.Create>(k == notPublished ? "Not Published" : k, g.ToList())).ToListAsync(); 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /src/Bloggy/Pages/Index.cshtml.cs: -------------------------------------------------------------------------------- 1 | using Bloggy.Models; 2 | using Microsoft.AspNetCore.Mvc.RazorPages; 3 | using Microsoft.EntityFrameworkCore; 4 | using Microsoft.Extensions.Options; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using System.Threading.Tasks; 8 | 9 | namespace Bloggy.Pages 10 | { 11 | public class IndexModel : PageModel 12 | { 13 | private readonly BloggingContext _db; 14 | private Blog _blog; 15 | 16 | public IndexModel(BloggingContext db, IOptions appSettings) 17 | { 18 | _db = db; 19 | _blog = appSettings.Value.Blog; 20 | } 21 | 22 | public IList Posts { get; private set; } 23 | 24 | public async Task OnGetAsync() 25 | { 26 | var postsNo = _blog.PostsPerPage; 27 | Posts = await _db.Posts 28 | .Where(p => p.IsPublished) 29 | .OrderByDescending(p => p.PublishedAt) 30 | .Take(postsNo) 31 | .ToListAsync(); 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /src/Bloggy/Pages/Account/Login.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | 3 | @model LoginModel 4 | 5 | @{ 6 | ViewBag.Title = "Log In"; 7 | } 8 | 9 |
10 |

@ViewBag.Title

11 |
12 |
13 |
14 |
15 |
16 | 17 | 18 |
19 |
20 | 21 | 22 |
23 | 24 |
25 |
26 |
27 |
-------------------------------------------------------------------------------- /src/Bloggy/Pages/Account/Posts/Create.cshtml.cs: -------------------------------------------------------------------------------- 1 | using Bloggy.Models; 2 | using Microsoft.AspNetCore.Mvc; 3 | using Microsoft.AspNetCore.Mvc.RazorPages; 4 | using System; 5 | using System.Threading.Tasks; 6 | 7 | namespace Bloggy.Pages.Account.Posts 8 | { 9 | public class CreatePostModel : PageModel 10 | { 11 | private readonly BloggingContext _db; 12 | 13 | public CreatePostModel(BloggingContext db) 14 | { 15 | _db = db; 16 | } 17 | 18 | [BindProperty] 19 | public Post Post { get; set; } 20 | 21 | [ValidateAntiForgeryToken] 22 | public async Task OnPostAsync() 23 | { 24 | if (!ModelState.IsValid) 25 | { 26 | return Page(); 27 | } 28 | 29 | if (Post.IsPublished) 30 | { 31 | Post.PublishedAt = DateTime.Now; 32 | } 33 | _db.Posts.Add(Post); 34 | await _db.SaveChangesAsync(); 35 | 36 | return RedirectToPage("/Index"); 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /src/Bloggy/Pages/_CommentFormPartial.cshtml: -------------------------------------------------------------------------------- 1 | @using Bloggy.Models 2 | 3 | @model Comment 4 | 5 |
6 | 7 |
8 | 9 | 10 |
11 |
12 | 13 | 14 | (will show your gravatar icon) 15 |
16 |
17 | 18 | 19 |
20 |
21 | 22 | 23 |
24 | 25 |
-------------------------------------------------------------------------------- /src/Bloggy/Pages/Tags.cshtml.cs: -------------------------------------------------------------------------------- 1 | using Bloggy.Models; 2 | using Microsoft.AspNetCore.Mvc.RazorPages; 3 | using Microsoft.EntityFrameworkCore; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using System.Threading.Tasks; 8 | 9 | namespace Bloggy.Pages 10 | { 11 | public class TagsModel : PageModel 12 | { 13 | private readonly BloggingContext _db; 14 | 15 | public TagsModel(BloggingContext db) 16 | { 17 | _db = db; 18 | } 19 | 20 | public IList Tags { get; set; } 21 | 22 | public async Task OnGetAsync() 23 | { 24 | Tags = await _db.Posts 25 | .SelectMany(p => p.Tags.Split(',', StringSplitOptions.RemoveEmptyEntries)) 26 | .Distinct() 27 | .OrderBy(t => t) 28 | .ToListAsync(); 29 | } 30 | 31 | public IList GetPosts(string tag) 32 | { 33 | return _db.Posts 34 | .Where(p => p.Tags.Contains(tag)) 35 | .OrderBy(p => p.PublishedAt) 36 | .ToList(); 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /src/Bloggy/TagHelpers/AlertTagHelper.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Razor.TagHelpers; 2 | 3 | namespace Bloggy.TagHelpers 4 | { 5 | [HtmlTargetElement("alert")] 6 | public class AlertTagHelper : TagHelper 7 | { 8 | public AlertType Type { get; set; } 9 | 10 | public override void Process(TagHelperContext context, TagHelperOutput output) 11 | { 12 | var @class = "alert alert-"; 13 | output.TagName = "div"; 14 | 15 | switch (Type) 16 | { 17 | case AlertType.Error: 18 | @class += "danger"; 19 | break; 20 | case AlertType.Information: 21 | @class += "info"; 22 | break; 23 | case AlertType.Success: 24 | @class += "success"; 25 | break; 26 | case AlertType.Warning: 27 | @class += "warning"; 28 | break; 29 | } 30 | output.Attributes.Add("class", @class); 31 | output.Attributes.Add("role", "alert"); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Bloggy/TagHelpers/PartialTagHelper.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Microsoft.AspNetCore.Mvc.Rendering; 3 | using Microsoft.AspNetCore.Mvc.ViewFeatures; 4 | using Microsoft.AspNetCore.Razor.TagHelpers; 5 | 6 | namespace Bloggy.TagHelpers 7 | { 8 | [HtmlTargetElement("partial", Attributes = "name")] 9 | public class PartialTagHelper : TagHelper 10 | { 11 | private readonly IHtmlHelper _htmlHelper; 12 | 13 | public PartialTagHelper(IHtmlHelper htmlHelper) 14 | { 15 | _htmlHelper = htmlHelper; 16 | } 17 | 18 | [ViewContext] 19 | public ViewContext ViewContext { get; set; } 20 | 21 | public string Name { get; set; } 22 | 23 | public object Model { get; set; } 24 | 25 | public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output) 26 | { 27 | ((IViewContextAware)_htmlHelper).Contextualize(ViewContext); 28 | 29 | output.TagName = null; 30 | 31 | var content = await _htmlHelper.PartialAsync(Name, Model); 32 | output.Content.SetHtmlContent(content); 33 | } 34 | } 35 | } -------------------------------------------------------------------------------- /src/Bloggy/TagHelpers/GravatarTagHelper.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Razor.TagHelpers; 2 | using System.Security.Cryptography; 3 | using System.Text; 4 | 5 | namespace Bloggy.TagHelpers 6 | { 7 | [HtmlTargetElement("gravatar", TagStructure = TagStructure.NormalOrSelfClosing)] 8 | public class GravatarTagHelper : TagHelper 9 | { 10 | public string Email { get; set; } 11 | 12 | public override void Process(TagHelperContext context, TagHelperOutput output) 13 | { 14 | output.TagName = null; 15 | 16 | var hash = ComputeHash(Email); 17 | output.Content.SetHtmlContent($""); 18 | } 19 | 20 | private string ComputeHash(string email) 21 | { 22 | var md5 = MD5.Create(); 23 | var bytes = Encoding.ASCII.GetBytes(email); 24 | var hash = md5.ComputeHash(bytes); 25 | var sb = new StringBuilder(); 26 | for (int i = 0; i < hash.Length; i++) 27 | { 28 | sb.Append(hash[i].ToString("x2")); 29 | } 30 | 31 | return sb.ToString(); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Bloggy/wwwroot/scripts/codemirror/tablist.js: -------------------------------------------------------------------------------- 1 | // CodeMirror, copyright (c) by Marijn Haverbeke and others 2 | // Distributed under an MIT license: http://codemirror.net/LICENSE 3 | 4 | var CodeMirror = require("codemirror"); 5 | 6 | CodeMirror.commands.tabAndIndentMarkdownList = function (cm) { 7 | var ranges = cm.listSelections(); 8 | var pos = ranges[0].head; 9 | var eolState = cm.getStateAfter(pos.line); 10 | var inList = eolState.list !== false; 11 | 12 | if (inList) { 13 | cm.execCommand("indentMore"); 14 | return; 15 | } 16 | 17 | if (cm.options.indentWithTabs) { 18 | cm.execCommand("insertTab"); 19 | } 20 | else { 21 | var spaces = Array(cm.options.tabSize + 1).join(" "); 22 | cm.replaceSelection(spaces); 23 | } 24 | }; 25 | 26 | CodeMirror.commands.shiftTabAndUnindentMarkdownList = function (cm) { 27 | var ranges = cm.listSelections(); 28 | var pos = ranges[0].head; 29 | var eolState = cm.getStateAfter(pos.line); 30 | var inList = eolState.list !== false; 31 | 32 | if (inList) { 33 | cm.execCommand("indentLess"); 34 | return; 35 | } 36 | 37 | if (cm.options.indentWithTabs) { 38 | cm.execCommand("insertTab"); 39 | } 40 | else { 41 | var spaces = Array(cm.options.tabSize + 1).join(" "); 42 | cm.replaceSelection(spaces); 43 | } 44 | }; 45 | -------------------------------------------------------------------------------- /src/Bloggy/TagHelpers/MarkdownTagHelper.cs: -------------------------------------------------------------------------------- 1 | using CommonMark; 2 | using Microsoft.AspNetCore.Mvc.ViewFeatures; 3 | using Microsoft.AspNetCore.Razor.TagHelpers; 4 | using System.Threading.Tasks; 5 | 6 | namespace Bloggy.TagHelpers 7 | { 8 | [HtmlTargetElement("markdown", TagStructure = TagStructure.NormalOrSelfClosing)] 9 | [HtmlTargetElement(Attributes = "markdown")] 10 | public class MarkdownTagHelper : TagHelper 11 | { 12 | public ModelExpression Content { get; set; } 13 | 14 | public async override Task ProcessAsync(TagHelperContext context, TagHelperOutput output) 15 | { 16 | if (output.TagName == "markdown") 17 | { 18 | output.TagName = null; 19 | } 20 | output.Attributes.RemoveAll("markdown"); 21 | 22 | var content = await GetContent(output); 23 | var markdown = content; 24 | var html = CommonMarkConverter.Convert(markdown); 25 | output.Content.SetHtmlContent(html ?? string.Empty); 26 | } 27 | 28 | private async Task GetContent(TagHelperOutput output) 29 | { 30 | if (Content == null) 31 | return (await output.GetChildContentAsync()).GetContent(); 32 | 33 | return Content.Model?.ToString(); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Bloggy.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.26228.4 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{C119D035-FEE1-4DBF-9046-2C1329F2DE0B}" 7 | EndProject 8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{6C77F36F-50BA-402B-88C1-73A8341FF76C}" 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Bloggy", "src\Bloggy\Bloggy.csproj", "{77A2C865-D54E-49BD-A25E-37DF2334C17E}" 11 | EndProject 12 | Global 13 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 14 | Debug|Any CPU = Debug|Any CPU 15 | Release|Any CPU = Release|Any CPU 16 | EndGlobalSection 17 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 18 | {77A2C865-D54E-49BD-A25E-37DF2334C17E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 19 | {77A2C865-D54E-49BD-A25E-37DF2334C17E}.Debug|Any CPU.Build.0 = Debug|Any CPU 20 | {77A2C865-D54E-49BD-A25E-37DF2334C17E}.Release|Any CPU.ActiveCfg = Release|Any CPU 21 | {77A2C865-D54E-49BD-A25E-37DF2334C17E}.Release|Any CPU.Build.0 = Release|Any CPU 22 | EndGlobalSection 23 | GlobalSection(SolutionProperties) = preSolution 24 | HideSolutionNode = FALSE 25 | EndGlobalSection 26 | GlobalSection(NestedProjects) = preSolution 27 | {77A2C865-D54E-49BD-A25E-37DF2334C17E} = {C119D035-FEE1-4DBF-9046-2C1329F2DE0B} 28 | EndGlobalSection 29 | EndGlobal 30 | -------------------------------------------------------------------------------- /src/Bloggy/Pages/Account/Posts/Edit.cshtml.cs: -------------------------------------------------------------------------------- 1 | using Bloggy.Models; 2 | using Microsoft.AspNetCore.Mvc; 3 | using Microsoft.AspNetCore.Mvc.RazorPages; 4 | using Microsoft.EntityFrameworkCore; 5 | using System; 6 | using System.Linq; 7 | using System.Threading.Tasks; 8 | 9 | namespace Bloggy.Pages.Account.Posts 10 | { 11 | public class EditPostModel : PageModel 12 | { 13 | private readonly BloggingContext _db; 14 | 15 | public EditPostModel(BloggingContext db) 16 | { 17 | _db = db; 18 | } 19 | 20 | [BindProperty] 21 | public Post Post { get; set; } 22 | 23 | public IActionResult OnGet(int id) 24 | { 25 | Post = _db.Posts.SingleOrDefault(p => p.Id == id); 26 | 27 | if (Post != null) 28 | { 29 | return Page(); 30 | } 31 | 32 | return RedirectToPage("/Error"); 33 | } 34 | 35 | [ValidateAntiForgeryToken] 36 | public async Task OnPostAsync() 37 | { 38 | if (!ModelState.IsValid) 39 | { 40 | return Page(); 41 | } 42 | 43 | if (Post.IsPublished) 44 | { 45 | Post.PublishedAt = DateTime.Now; 46 | } 47 | Post.LastModified = DateTime.Now; 48 | _db.Entry(Post).State = EntityState.Modified; 49 | await _db.SaveChangesAsync(); 50 | 51 | return RedirectToPage("/Post", new { slug = Post.Slug}); 52 | } 53 | } 54 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Bloggy 2 | 3 | ## Why another blog engine? 4 | 5 | > It's always time to start a new blog engine 6 | 7 | That's the retweet from Mads Kristensen @madskristensen after I tweeted him on 7 May 2016 to start a new blog engine using the latest and greates technologies. Unfortunately he don't have a time!! I'm sure that he is busy with building a new cool extensions for Visual Studio as usual :) 8 | 9 | I take the initiative to continue with what he built last few years in both BlogEngine & MiniBlog, so I decide to build a new blog engine called **bloggy** using the new ASP.NET Core. 10 | 11 | I'm sure this is a great lesson for me to use almost the things that I know and love in ASP.NET Core to build such application. 12 | 13 | ## Features 14 | 15 | - Performance 16 | - Response Compression 17 | - Response Caching 18 | - Support CDN for scripts & styles in Production Environment 19 | - Authentication 20 | - Cookie-based authentication 21 | - Social Media authentication 22 | - Configuration 23 | - Almost the blog settings are configurable in appsettings.json 24 | - Themable 25 | - Easy to create and customize the themes 26 | - The conjunction with localization make it easy to support different layouts LTR & RTL 27 | - Comes with light & black theme 28 | - Localization 29 | - Support both English & Arabic out of the box 30 | - Url Culture Provider 31 | - Schedule posts to be published on a future date 32 | - Comments 33 | - Gravatar support 34 | - reCaptcha support 35 | - Comment moderation 36 | - Live preview on commenting 37 | - Data Store 38 | - Shipped with InMemory (for Testing) & SQLite, but you can easily switch to SQL Server or any EntityFramework Database Provider 39 | - Web Feeds 40 | - RSS 41 | - ATOM 42 | - Markdown support as first class citizen 43 | - Mobile friendly 44 | - Works on Azure -------------------------------------------------------------------------------- /src/Bloggy/Pages/Post.cshtml: -------------------------------------------------------------------------------- 1 | @page "{slug}" 2 | 3 | @model PostModel 4 | 5 | @{ 6 | ViewBag.Title = Model.Post.Slug; 7 | } 8 | 9 | 15 | 16 |
17 |

@Model.Post.Comments.Count() Comments

18 | @foreach (var comment in Model.Post.Comments) 19 | { 20 |
21 |
22 | 23 |
24 |

@comment.Author

25 | 26 |

@Html.Raw(comment.Content)

27 |
28 | @if (User.Identity.IsAuthenticated) 29 | { 30 |
31 | [ Edit | ] 32 |
33 | } 34 |
35 |
36 | } 37 |
38 | 39 | @if (Model.Blog.AllowComments && (DateTime.Now - Model.Post.PublishedAt).Days <= Model.Blog.DaysToComment) 40 | { 41 | if (Model.ShowErrorMessage) 42 | { 43 | @Model.ErrorMessage 44 | } 45 | 46 | if (Model.ShowSuccessMessage) 47 | { 48 | @Model.SuccessMessage 49 | } 50 | else 51 | { 52 | 53 | } 54 | } -------------------------------------------------------------------------------- /src/Bloggy/Pages/Account/Posts/Create.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | 3 | @model CreatePostModel 4 | 5 | @{ 6 | ViewBag.Title = "Create Post"; 7 | } 8 | 9 |
10 |

@ViewBag.Title

11 |
12 |
13 |
14 |
15 |
16 | 17 | 18 |
19 |
20 | 21 | 22 |
23 |
24 | 25 | 26 |
27 |
28 | 29 | 30 |
31 |
32 | 33 | 34 |
35 |
36 | 39 |
40 | 41 |
42 |
43 |
44 |
-------------------------------------------------------------------------------- /src/Bloggy/ThemeViewLocationExpander.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc.Razor; 2 | using Microsoft.Extensions.Options; 3 | using System; 4 | using System.Collections.Generic; 5 | 6 | namespace Bloggy 7 | { 8 | public class ThemeViewLocationExpander : IViewLocationExpander 9 | { 10 | private const string ValueKey = "theme"; 11 | 12 | public IEnumerable ExpandViewLocations(ViewLocationExpanderContext context, IEnumerable viewLocations) 13 | { 14 | if (context == null) 15 | { 16 | throw new ArgumentNullException(nameof(context)); 17 | } 18 | 19 | if (viewLocations == null) 20 | { 21 | throw new ArgumentNullException(nameof(viewLocations)); 22 | } 23 | 24 | context.Values.TryGetValue(ValueKey, out string theme); 25 | 26 | if (!string.IsNullOrEmpty(theme)) 27 | { 28 | return ExpandViewLocationsCore(viewLocations, theme); 29 | } 30 | 31 | return viewLocations; 32 | } 33 | 34 | public void PopulateValues(ViewLocationExpanderContext context) 35 | { 36 | if (context == null) 37 | { 38 | throw new ArgumentNullException(nameof(context)); 39 | } 40 | 41 | var appSettings = context.ActionContext.HttpContext.RequestServices 42 | .GetService(typeof(IOptions)) as IOptions; 43 | 44 | context.Values[ValueKey] = appSettings.Value.Blog.Theme; 45 | } 46 | 47 | private IEnumerable ExpandViewLocationsCore(IEnumerable viewLocations, string theme) 48 | { 49 | foreach (var location in viewLocations) 50 | { 51 | yield return location; 52 | yield return location.Replace("/Pages/", $"/Pages/Themes/{theme}/"); 53 | } 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Bloggy/Pages/Account/Posts/Edit.cshtml: -------------------------------------------------------------------------------- 1 | @page "{id:int}" 2 | 3 | @model EditPostModel 4 | 5 | @{ 6 | ViewBag.Title = "Edit Post"; 7 | } 8 | 9 |
10 |

@ViewBag.Title

11 |
12 |
13 |
14 |
15 | 16 |
17 | 18 | 19 |
20 |
21 | 22 | 23 |
24 |
25 | 26 | 27 |
28 |
29 | 30 | 31 |
32 |
33 | 34 | 35 |
36 |
37 | 40 |
41 | 42 |
43 |
44 |
45 |
-------------------------------------------------------------------------------- /src/Bloggy/Migrations/BloggingContextModelSnapshot.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.EntityFrameworkCore; 3 | using Microsoft.EntityFrameworkCore.Infrastructure; 4 | using Microsoft.EntityFrameworkCore.Metadata; 5 | using Microsoft.EntityFrameworkCore.Migrations; 6 | using Bloggy.Models; 7 | 8 | namespace Bloggy.Migrations 9 | { 10 | [DbContext(typeof(BloggingContext))] 11 | partial class BloggingContextModelSnapshot : ModelSnapshot 12 | { 13 | protected override void BuildModel(ModelBuilder modelBuilder) 14 | { 15 | modelBuilder 16 | .HasAnnotation("ProductVersion", "1.1.0-rtm-22752"); 17 | 18 | modelBuilder.Entity("Bloggy.Models.Comment", b => 19 | { 20 | b.Property("Id") 21 | .ValueGeneratedOnAdd(); 22 | 23 | b.Property("Author") 24 | .IsRequired() 25 | .HasMaxLength(100); 26 | 27 | b.Property("Content") 28 | .IsRequired(); 29 | 30 | b.Property("Email") 31 | .IsRequired(); 32 | 33 | b.Property("PostId"); 34 | 35 | b.Property("PublishedAt"); 36 | 37 | b.Property("Website"); 38 | 39 | b.HasKey("Id"); 40 | 41 | b.HasIndex("PostId"); 42 | 43 | b.ToTable("Comments"); 44 | }); 45 | 46 | modelBuilder.Entity("Bloggy.Models.Post", b => 47 | { 48 | b.Property("Id") 49 | .ValueGeneratedOnAdd(); 50 | 51 | b.Property("Content"); 52 | 53 | b.Property("Excerpt"); 54 | 55 | b.Property("IsPublished"); 56 | 57 | b.Property("LastModified"); 58 | 59 | b.Property("PublishedAt"); 60 | 61 | b.Property("Slug"); 62 | 63 | b.Property("Tags"); 64 | 65 | b.Property("Title"); 66 | 67 | b.HasKey("Id"); 68 | 69 | b.ToTable("Posts"); 70 | }); 71 | 72 | modelBuilder.Entity("Bloggy.Models.Comment", b => 73 | { 74 | b.HasOne("Bloggy.Models.Post", "Post") 75 | .WithMany("Comments") 76 | .HasForeignKey("PostId") 77 | .OnDelete(DeleteBehavior.Cascade); 78 | }); 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/Bloggy/Migrations/20161227000614_Initial.Designer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.EntityFrameworkCore; 3 | using Microsoft.EntityFrameworkCore.Infrastructure; 4 | using Microsoft.EntityFrameworkCore.Metadata; 5 | using Microsoft.EntityFrameworkCore.Migrations; 6 | using Bloggy.Models; 7 | 8 | namespace Bloggy.Migrations 9 | { 10 | [DbContext(typeof(BloggingContext))] 11 | [Migration("20161227000614_Initial")] 12 | partial class Initial 13 | { 14 | protected override void BuildTargetModel(ModelBuilder modelBuilder) 15 | { 16 | modelBuilder 17 | .HasAnnotation("ProductVersion", "1.1.0-rtm-22752"); 18 | 19 | modelBuilder.Entity("Bloggy.Models.Comment", b => 20 | { 21 | b.Property("Id") 22 | .ValueGeneratedOnAdd(); 23 | 24 | b.Property("Author") 25 | .IsRequired() 26 | .HasMaxLength(100); 27 | 28 | b.Property("Content") 29 | .IsRequired(); 30 | 31 | b.Property("Email") 32 | .IsRequired(); 33 | 34 | b.Property("PostId"); 35 | 36 | b.Property("PublishedAt"); 37 | 38 | b.Property("Website"); 39 | 40 | b.HasKey("Id"); 41 | 42 | b.HasIndex("PostId"); 43 | 44 | b.ToTable("Comments"); 45 | }); 46 | 47 | modelBuilder.Entity("Bloggy.Models.Post", b => 48 | { 49 | b.Property("Id") 50 | .ValueGeneratedOnAdd(); 51 | 52 | b.Property("Content"); 53 | 54 | b.Property("Excerpt"); 55 | 56 | b.Property("IsPublished"); 57 | 58 | b.Property("LastModified"); 59 | 60 | b.Property("PublishedAt"); 61 | 62 | b.Property("Slug"); 63 | 64 | b.Property("Tags"); 65 | 66 | b.Property("Title"); 67 | 68 | b.HasKey("Id"); 69 | 70 | b.ToTable("Posts"); 71 | }); 72 | 73 | modelBuilder.Entity("Bloggy.Models.Comment", b => 74 | { 75 | b.HasOne("Bloggy.Models.Post", "Post") 76 | .WithMany("Comments") 77 | .HasForeignKey("PostId") 78 | .OnDelete(DeleteBehavior.Cascade); 79 | }); 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/Bloggy/Pages/Post.cshtml.cs: -------------------------------------------------------------------------------- 1 | using Bloggy.Models; 2 | using Microsoft.AspNetCore.Mvc; 3 | using Microsoft.AspNetCore.Mvc.RazorPages; 4 | using Microsoft.AspNetCore.Mvc.ViewFeatures; 5 | using Microsoft.EntityFrameworkCore; 6 | using Microsoft.Extensions.Options; 7 | using System; 8 | using System.Linq; 9 | using System.Threading.Tasks; 10 | 11 | namespace Bloggy.Pages 12 | { 13 | public class PostModel : PageModel 14 | { 15 | private readonly BloggingContext _db; 16 | private Blog _blog; 17 | 18 | public PostModel(BloggingContext db, IOptions appSettings) 19 | { 20 | _db = db; 21 | _blog = appSettings.Value.Blog; 22 | } 23 | 24 | public Post Post { get; private set; } 25 | 26 | public Blog Blog => _blog; 27 | 28 | [BindProperty] 29 | public Comment Comment { get; set; } 30 | 31 | [TempData] 32 | public string SuccessMessage { get; set; } 33 | 34 | public bool ShowSuccessMessage => !string.IsNullOrEmpty(SuccessMessage); 35 | 36 | [TempData] 37 | public string ErrorMessage { get; set; } 38 | 39 | public bool ShowErrorMessage => !string.IsNullOrEmpty(ErrorMessage); 40 | 41 | public IActionResult OnGet(string slug) 42 | { 43 | Post = _db.Posts.Include(p => p.Comments) 44 | .SingleOrDefault(p => p.Slug.Equals(slug, StringComparison.OrdinalIgnoreCase)); 45 | 46 | if (Post != null) 47 | { 48 | return Page(); 49 | } 50 | 51 | return RedirectToPage("/Error"); 52 | } 53 | 54 | public async Task OnPostAddCommentAsync(string slug) 55 | { 56 | Post = _db.Posts.Include(p => p.Comments) 57 | .Single(p => p.Slug.Equals(slug, StringComparison.OrdinalIgnoreCase)); 58 | 59 | if (!ModelState.IsValid) 60 | { 61 | ErrorMessage = "Unable to add comment"; 62 | return Page(); 63 | } 64 | 65 | Comment.PostId = Post.Id; 66 | Comment.PublishedAt = DateTime.UtcNow; 67 | Comment.Content = Comment.Content.Replace(Environment.NewLine, "
"); 68 | _db.Comments.Add(Comment); 69 | await _db.SaveChangesAsync(); 70 | SuccessMessage = "Your comment has been added"; 71 | 72 | return RedirectToPage(); 73 | } 74 | 75 | public async Task OnPostDeleteCommentAsync(int id) 76 | { 77 | var comment = _db.Comments.Include(c => c.Post) 78 | .SingleOrDefault(c => c.Id == id); 79 | 80 | if (comment != null) 81 | { 82 | _db.Comments.Remove(comment); 83 | await _db.SaveChangesAsync(); 84 | } 85 | 86 | return RedirectToPage(); 87 | } 88 | } 89 | } -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Make sh files under the build directory always have LF as line endings 8 | ############################################################################### 9 | build/*.sh eol=lf 10 | 11 | 12 | ############################################################################### 13 | # Set default behavior for command prompt diff. 14 | # 15 | # This is need for earlier builds of msysgit that does not have it on by 16 | # default for csharp files. 17 | # Note: This is only used by command line 18 | ############################################################################### 19 | #*.cs diff=csharp 20 | 21 | ############################################################################### 22 | # Set the merge driver for project and solution files 23 | # 24 | # Merging from the command prompt will add diff markers to the files if there 25 | # are conflicts (Merging from VS is not affected by the settings below, in VS 26 | # the diff markers are never inserted). Diff markers may cause the following 27 | # file extensions to fail to load in VS. An alternative would be to treat 28 | # these files as binary and thus will always conflict and require user 29 | # intervention with every merge. To do so, just uncomment the entries below 30 | ############################################################################### 31 | #*.sln merge=binary 32 | #*.csproj merge=binary 33 | #*.vbproj merge=binary 34 | #*.vcxproj merge=binary 35 | #*.vcproj merge=binary 36 | #*.dbproj merge=binary 37 | #*.fsproj merge=binary 38 | #*.lsproj merge=binary 39 | #*.wixproj merge=binary 40 | #*.modelproj merge=binary 41 | #*.sqlproj merge=binary 42 | #*.wwaproj merge=binary 43 | 44 | ############################################################################### 45 | # behavior for image files 46 | # 47 | # image files are treated as binary by default. 48 | ############################################################################### 49 | #*.jpg binary 50 | #*.png binary 51 | #*.gif binary 52 | 53 | ############################################################################### 54 | # diff behavior for common document formats 55 | # 56 | # Convert binary document formats to text before diffing them. This feature 57 | # is only available from the command line. Turn it on by uncommenting the 58 | # entries below. 59 | ############################################################################### 60 | #*.doc diff=astextplain 61 | #*.DOC diff=astextplain 62 | #*.docx diff=astextplain 63 | #*.DOCX diff=astextplain 64 | #*.dot diff=astextplain 65 | #*.DOT diff=astextplain 66 | #*.pdf diff=astextplain 67 | #*.PDF diff=astextplain 68 | #*.rtf diff=astextplain 69 | #*.RTF diff=astextplain 70 | -------------------------------------------------------------------------------- /src/Bloggy/Pages/Themes/Dark/_Layout.cshtml: -------------------------------------------------------------------------------- 1 | @using Bloggy 2 | @using Bloggy.Models 3 | @using Microsoft.Extensions.Options 4 | 5 | @inject IOptions AppSettings 6 | 7 | @{ 8 | var activePage = ViewContext.RouteData.Values["page"].ToString(); 9 | } 10 | 11 | @functions 12 | { 13 | public Blog Blog 14 | { 15 | get 16 | { 17 | return AppSettings.Value.Blog; 18 | } 19 | } 20 | } 21 | 22 | 23 | 24 | 25 | 26 | 27 | @Blog.Name - @ViewBag.Title 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 |
45 |
46 | 47 |

@Blog.Name

48 | 54 |
55 | 56 |
57 |
58 |
59 | @RenderBody() 60 |
61 |
62 |

Copyright © @DateTime.UtcNow.Year @Blog.Name

63 |
64 |
65 | 71 | 72 | -------------------------------------------------------------------------------- /src/Bloggy/Pages/Themes/Light/_Layout.cshtml: -------------------------------------------------------------------------------- 1 | @using Bloggy 2 | @using Bloggy.Models 3 | @using Microsoft.Extensions.Options 4 | 5 | @inject IOptions AppSettings 6 | 7 | @{ 8 | var activePage = ViewContext.RouteData.Values["page"].ToString(); 9 | } 10 | 11 | @functions 12 | { 13 | public Blog Blog 14 | { 15 | get 16 | { 17 | return AppSettings.Value.Blog; 18 | } 19 | } 20 | } 21 | 22 | 23 | 24 | 25 | 26 | 27 | @Blog.Name - @ViewBag.Title 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 |
45 |
46 | 47 |

@Blog.Name

48 | 54 |
55 | 56 |
57 |
58 |
59 | @RenderBody() 60 |
61 |
62 |

Copyright © @DateTime.UtcNow.Year @Blog.Name

63 |
64 |
65 | 71 | 72 | -------------------------------------------------------------------------------- /src/Bloggy/Migrations/20161227000614_Initial.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Microsoft.EntityFrameworkCore.Migrations; 4 | 5 | namespace Bloggy.Migrations 6 | { 7 | public partial class Initial : Migration 8 | { 9 | protected override void Up(MigrationBuilder migrationBuilder) 10 | { 11 | migrationBuilder.CreateTable( 12 | name: "Posts", 13 | columns: table => new 14 | { 15 | Id = table.Column(nullable: false) 16 | .Annotation("Sqlite:Autoincrement", true), 17 | Content = table.Column(nullable: true), 18 | Excerpt = table.Column(nullable: true), 19 | IsPublished = table.Column(nullable: false), 20 | LastModified = table.Column(nullable: false), 21 | PublishedAt = table.Column(nullable: true), 22 | Slug = table.Column(nullable: true), 23 | Tags = table.Column(nullable: true), 24 | Title = table.Column(nullable: true) 25 | }, 26 | constraints: table => 27 | { 28 | table.PrimaryKey("PK_Posts", x => x.Id); 29 | }); 30 | 31 | migrationBuilder.CreateTable( 32 | name: "Comments", 33 | columns: table => new 34 | { 35 | Id = table.Column(nullable: false) 36 | .Annotation("Sqlite:Autoincrement", true), 37 | Author = table.Column(maxLength: 100, nullable: false), 38 | Content = table.Column(nullable: false), 39 | Email = table.Column(nullable: false), 40 | PostId = table.Column(nullable: false), 41 | PublishedAt = table.Column(nullable: false), 42 | Website = table.Column(nullable: true) 43 | }, 44 | constraints: table => 45 | { 46 | table.PrimaryKey("PK_Comments", x => x.Id); 47 | table.ForeignKey( 48 | name: "FK_Comments_Posts_PostId", 49 | column: x => x.PostId, 50 | principalTable: "Posts", 51 | principalColumn: "Id", 52 | onDelete: ReferentialAction.Cascade); 53 | }); 54 | 55 | migrationBuilder.CreateIndex( 56 | name: "IX_Comments_PostId", 57 | table: "Comments", 58 | column: "PostId"); 59 | } 60 | 61 | protected override void Down(MigrationBuilder migrationBuilder) 62 | { 63 | migrationBuilder.DropTable( 64 | name: "Comments"); 65 | 66 | migrationBuilder.DropTable( 67 | name: "Posts"); 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/Bloggy/Pages/Account/Login.cshtml.cs: -------------------------------------------------------------------------------- 1 | using Bloggy.Models; 2 | using Microsoft.AspNetCore.Authentication; 3 | using Microsoft.AspNetCore.Authentication.Cookies; 4 | using Microsoft.AspNetCore.Mvc; 5 | using Microsoft.AspNetCore.Mvc.RazorPages; 6 | using Microsoft.Extensions.Options; 7 | using System; 8 | using System.Security.Claims; 9 | using System.Security.Cryptography; 10 | using System.Text; 11 | using System.Threading.Tasks; 12 | 13 | namespace Bloggy.Pages.Account 14 | { 15 | public class LoginModel : PageModel 16 | { 17 | private Credential _credential; 18 | 19 | public LoginModel(IOptions appSettings) 20 | { 21 | _credential = appSettings.Value.Credential; 22 | } 23 | 24 | [BindProperty] 25 | public Bloggy.Models.User AdminUser { get; set; } 26 | 27 | public IActionResult OnGet(string returnUrl = null) 28 | { 29 | ViewData["ReturnUrl"] = returnUrl; 30 | return Page(); 31 | } 32 | 33 | [ValidateAntiForgeryToken] 34 | public async Task OnPost(string returnUrl = null) 35 | { 36 | ViewData["ReturnUrl"] = returnUrl; 37 | if (ModelState.IsValid) 38 | { 39 | if (_credential.PasswordFormat != PasswordFormat.SHA1) 40 | { 41 | throw new NotSupportedException(); 42 | } 43 | 44 | if (AdminUser.Name == _credential.User.Name && GetHashedPassword(AdminUser.Password).Equals(_credential.User.Password, StringComparison.OrdinalIgnoreCase)) 45 | { 46 | var adminUser = new ClaimsPrincipal( 47 | new ClaimsIdentity(new[] { new Claim(ClaimTypes.Name, _credential.User.Name) }, 48 | CookieAuthenticationDefaults.AuthenticationScheme)); 49 | await PageContext.HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, adminUser); 50 | 51 | if (string.IsNullOrEmpty(returnUrl)) 52 | { 53 | return RedirectToPage("/Index"); 54 | } 55 | else 56 | { 57 | return LocalRedirect(returnUrl); 58 | } 59 | } 60 | else 61 | { 62 | ModelState.AddModelError(string.Empty, "Invalid login attempt."); 63 | return Page(); 64 | } 65 | } 66 | 67 | return Page(); 68 | } 69 | 70 | private string GetHashedPassword(string password) 71 | { 72 | var bytes = Encoding.UTF8.GetBytes(password); 73 | var hashBytes = SHA1.Create().ComputeHash(bytes); 74 | 75 | return BitConverter.ToString(hashBytes).Replace("-", string.Empty); 76 | } 77 | 78 | //private IActionResult RedirectToLocal(string returnUrl) 79 | //{ 80 | // if (Url.IsLocalUrl(returnUrl)) 81 | // { 82 | // return LocalRedirect(returnUrl); 83 | // } 84 | // else 85 | // { 86 | // return RedirectToPage("/Index"); 87 | // } 88 | //} 89 | } 90 | } -------------------------------------------------------------------------------- /src/Bloggy/Startup.cs: -------------------------------------------------------------------------------- 1 | using Bloggy.Models; 2 | using Microsoft.AspNetCore.Authentication.Cookies; 3 | using Microsoft.AspNetCore.Builder; 4 | using Microsoft.AspNetCore.Hosting; 5 | using Microsoft.AspNetCore.Mvc.Razor; 6 | using Microsoft.AspNetCore.Mvc.ViewFeatures; 7 | using Microsoft.AspNetCore.ResponseCompression; 8 | using Microsoft.EntityFrameworkCore; 9 | using Microsoft.Extensions.Configuration; 10 | using Microsoft.Extensions.DependencyInjection; 11 | using Microsoft.Extensions.FileProviders; 12 | using Microsoft.Extensions.Logging; 13 | using Microsoft.Extensions.Options; 14 | using System; 15 | using System.Collections.Generic; 16 | using System.IO; 17 | using System.IO.Compression; 18 | 19 | namespace Bloggy 20 | { 21 | public class Startup 22 | { 23 | public Startup(IHostingEnvironment env) 24 | { 25 | Environment = env; 26 | var builder = new ConfigurationBuilder() 27 | .SetBasePath(Environment.ContentRootPath) 28 | .AddJsonFile("appsettings.json"); 29 | 30 | Configuration = builder.Build(); 31 | } 32 | 33 | public IConfigurationRoot Configuration { get; set; } 34 | 35 | public IHostingEnvironment Environment { get; set; } 36 | 37 | public BloggingContext Db { get; set; } 38 | 39 | public void ConfigureServices(IServiceCollection services) 40 | { 41 | if (Environment.IsDevelopment()) 42 | { 43 | services.AddDbContext(options => options.UseInMemoryDatabase("bloggy")); 44 | } 45 | else 46 | { 47 | services.AddDbContext(options => options.UseSqlite(Configuration["Data:DefaultConnection:ConnectionString"])); 48 | } 49 | 50 | services.Configure(options => options.Level = CompressionLevel.Fastest); 51 | services.AddResponseCompression(options => 52 | { 53 | options.Providers.Add(); 54 | }); 55 | 56 | services.Configure(options => Configuration.GetSection("AppSettings").Bind(options)); 57 | 58 | services.AddAuthentication(options => 59 | { 60 | options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme; 61 | options.DefaultChallengeScheme = CookieAuthenticationDefaults.AuthenticationScheme; 62 | }); 63 | 64 | services.AddAuthentication(options => 65 | { 66 | options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme; 67 | options.DefaultChallengeScheme = CookieAuthenticationDefaults.AuthenticationScheme; 68 | }).AddCookie(); 69 | 70 | services.AddMvc() 71 | .AddRazorPagesOptions(options => 72 | { 73 | options.Conventions.AuthorizeFolder("/Account"); 74 | options.Conventions.AllowAnonymousToPage("/Account/Login"); 75 | }); 76 | 77 | services.Configure(options => 78 | { 79 | options.ViewLocationExpanders.Add(new ThemeViewLocationExpander()); 80 | }); 81 | 82 | services.AddSingleton(); 83 | } 84 | 85 | public void Configure(IApplicationBuilder app, ILoggerFactory loggerFactory, BloggingContext db) 86 | { 87 | if (Environment.IsDevelopment()) 88 | { 89 | Db = db; 90 | loggerFactory.AddConsole(Configuration.GetSection("Logging")); 91 | app.UseDeveloperExceptionPage(); 92 | app.UseDatabaseErrorPage(); 93 | AddSeedData(); 94 | } 95 | else 96 | { 97 | app.UseExceptionHandler("/error"); 98 | } 99 | 100 | app.UseAuthentication(); 101 | 102 | app.UseResponseCompression(); 103 | 104 | app.UseStaticFiles(); 105 | 106 | var appSettings = app.ApplicationServices.GetRequiredService>(); 107 | 108 | app.UseStaticFiles(new StaticFileOptions() 109 | { 110 | FileProvider = new PhysicalFileProvider(Path.Combine(Directory.GetCurrentDirectory(), 111 | $@"Pages/Themes/{appSettings.Value.Blog.Theme}")), 112 | RequestPath = string.Empty 113 | }); 114 | 115 | app.UseMvcWithDefaultRoute(); 116 | } 117 | 118 | private void AddSeedData() 119 | { 120 | for (int i = 1; i <= 10; i++) 121 | { 122 | var post = new Post 123 | { 124 | Title = "What is Lorem Ipsum?", 125 | Slug = "what-is-lorem-ipsum-" + i, 126 | Excerpt = "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s", 127 | Content = @"**Lorem Ipsum** is simply dummy text of the printing and typesetting industry. **Lorem Ipsum** has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing **Lorem Ipsum** passages, and more recently with desktop publishing software like Aldus PageMaker including versions of **Lorem Ipsum**.", 128 | IsPublished = true, 129 | PublishedAt = new DateTime(2016, 12, 2), 130 | Tags = "Lorem Ipsum,lorem,ipsum" 131 | }; 132 | post.Comments = new List(); 133 | 134 | for (int j = 1; j < 3; j++) 135 | { 136 | var comment = new Comment() 137 | { 138 | Author = "Lorem Ipsum", 139 | Email = "lorem@ipsum.com", 140 | Content = "Lorem Ipsum is simply dummy text of the printing and typesetting industry.", 141 | Website = "http://www.lipsum.com", 142 | PublishedAt = new DateTime(2016, 12, 14), 143 | PostId = i, 144 | Post = post 145 | }; 146 | post.Comments.Add(comment); 147 | } 148 | Db.Posts.Add(post); 149 | } 150 | 151 | Db.SaveChanges(); 152 | } 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /src/Bloggy/Pages/Themes/Light/styles/bloggy.css: -------------------------------------------------------------------------------- 1 | /* Thin */ 2 | @font-face { 3 | font-family: "Roboto"; 4 | font-style: normal; 5 | font-weight: 100; 6 | src: url('../fonts/Roboto-Thin.ttf') format('truetype'); 7 | } 8 | 9 | @font-face { 10 | font-family: "Roboto"; 11 | font-style: italic; 12 | font-weight: 100; 13 | src: url('../fonts/Roboto-ThinItalic.ttf') format('truetype'); 14 | } 15 | 16 | /* Light */ 17 | @font-face { 18 | font-family: "Roboto"; 19 | font-style: normal; 20 | font-weight: 300; 21 | src: url('../fonts/Roboto-Light.ttf') format('truetype'); 22 | } 23 | 24 | @font-face { 25 | font-family: "Roboto"; 26 | font-style: italic; 27 | font-weight: 300; 28 | src: url('../fonts/Roboto-LightItalic.ttf') format('truetype'); 29 | } 30 | 31 | /* Normal */ 32 | @font-face { 33 | font-family: "Roboto"; 34 | font-style: normal; 35 | font-weight: 400; 36 | src: url('../fonts/Roboto-Regular.ttf') format('truetype'); 37 | } 38 | 39 | @font-face { 40 | font-family: "Roboto"; 41 | font-style: italic; 42 | font-weight: 400; 43 | src: url('../fonts/Roboto-Italic.ttf') format('truetype'); 44 | } 45 | 46 | /* Medium */ 47 | @font-face { 48 | font-family: "Roboto"; 49 | font-style: normal; 50 | font-weight: 500; 51 | src: url('../fonts/Roboto-Medium.ttf') format('truetype'); 52 | } 53 | 54 | @font-face { 55 | font-family: "Roboto"; 56 | font-style: italic; 57 | font-weight: 500; 58 | src: url('../fonts/Roboto-MediumItalic.ttf') format('truetype'); 59 | } 60 | 61 | /* Bold */ 62 | @font-face { 63 | font-family: "Roboto"; 64 | font-style: normal; 65 | font-weight: 700; 66 | src: url('../fonts/Roboto-Bold.ttf') format('truetype'); 67 | } 68 | 69 | @font-face { 70 | font-family: "Roboto"; 71 | font-style: italic; 72 | font-weight: 700; 73 | src: url('../fonts/Roboto-BoldItalic.ttf') format('truetype'); 74 | } 75 | 76 | /* Black */ 77 | @font-face { 78 | font-family: "Roboto"; 79 | font-style: normal; 80 | font-weight: 900; 81 | src: url('../fonts/Roboto-Black.ttf') format('truetype'); 82 | } 83 | 84 | @font-face { 85 | font-family: "Roboto"; 86 | font-style: italic; 87 | font-weight: 900; 88 | src: url('../fonts/Roboto-BlackItalic.ttf') format('truetype'); 89 | } 90 | 91 | body { 92 | background-color: #F5F5F5; 93 | font-family: 'Roboto', sans-serif; 94 | } 95 | 96 | .container { 97 | width: 670px; 98 | } 99 | 100 | header { 101 | margin: 30px auto; 102 | text-align: center; 103 | } 104 | 105 | header h1 { 106 | font-size: 60px; 107 | font-weight: lighter; 108 | } 109 | 110 | header nav { 111 | margin: auto; 112 | text-align: center; 113 | margin-top: 30px; 114 | margin-bottom: 10px; 115 | } 116 | 117 | header nav a { 118 | color: #999; 119 | text-transform: uppercase; 120 | text-decoration: none; 121 | font-size: 18px; 122 | margin-right: 15px; 123 | } 124 | 125 | header nav a.active, a:hover { 126 | color: #111; 127 | text-decoration: none; 128 | } 129 | 130 | header a { 131 | color: #1BA0FF; 132 | text-decoration: none; 133 | } 134 | 135 | header a:hover { 136 | color: #111; 137 | text-decoration: none; 138 | } 139 | 140 | article { 141 | margin-bottom: 30px; 142 | } 143 | 144 | article a { 145 | color: #111; 146 | font-size: 28px; 147 | font-weight: normal; 148 | outline: none; 149 | } 150 | 151 | article a:hover { 152 | color: #1BA0FF; 153 | text-decoration: none; 154 | } 155 | 156 | article time { 157 | color: #999; 158 | } 159 | 160 | article span#tags > a { 161 | color: #1BA0FF; 162 | font-size: 16px; 163 | } 164 | 165 | article span#tags > a:hover { 166 | text-decoration: underline; 167 | } 168 | 169 | article p, time { 170 | font-size: 16px; 171 | line-height: 24px; 172 | } 173 | 174 | #comments { 175 | margin-left: -15px; 176 | } 177 | 178 | .comment { 179 | margin-top: 20px; 180 | padding-bottom: 20px; 181 | border-bottom: 1px solid #ddd; 182 | } 183 | 184 | .comment:nth-last-child(1) { 185 | border-bottom: none; 186 | } 187 | 188 | .comment img { 189 | float: left; 190 | width: 100px; 191 | padding-right: 15px; 192 | } 193 | 194 | .comment div { 195 | overflow: auto; 196 | } 197 | 198 | .comment a { 199 | color: #111; 200 | font-size: 20px; 201 | font-weight: normal; 202 | outline: none; 203 | } 204 | 205 | .comment a:hover { 206 | color: #1BA0FF; 207 | text-decoration: none; 208 | } 209 | 210 | .comment time { 211 | color: #999; 212 | display: block; 213 | } 214 | 215 | .comment p, time { 216 | font-size: 16px; 217 | line-height: 24px; 218 | } 219 | 220 | #commentForm, .alert-success, .alert-error { 221 | margin-left: -15px; 222 | margin-top: 20px; 223 | } 224 | 225 | #commentForm ul { 226 | margin-left: -25px; 227 | } 228 | 229 | #commentForm button[type=submit] { 230 | background-color: #1BA0FF; 231 | color: #fff; 232 | } 233 | 234 | #archive span.label, #tags span.label { 235 | background-color: #999; 236 | } 237 | 238 | #archive span.label:hover, #tags span.label:hover { 239 | background-color: #1BA0FF; 240 | } 241 | 242 | #archive h3, #tags h3 { 243 | color: #1BA0FF; 244 | } 245 | 246 | #archive li > a, #tags li > a { 247 | font-size: 16px; 248 | color: #111; 249 | text-decoration: underline; 250 | } 251 | 252 | #archive li > a:hover, #tags li > a:hover { 253 | color: #1BA0FF; 254 | } 255 | 256 | #contact a { 257 | color: #1BA0FF; 258 | text-decoration: none; 259 | font-size: 14px; 260 | } 261 | 262 | #contact a:hover { 263 | color: #111; 264 | } 265 | 266 | #login button[type=submit] { 267 | background-color: #1BA0FF; 268 | color: #fff; 269 | } 270 | 271 | #new-post { 272 | margin-left: -15px; 273 | } 274 | 275 | .actions { 276 | color: #000; 277 | } 278 | 279 | .actions > a { 280 | font-size: 14px; 281 | color: #1BA0FF; 282 | } 283 | 284 | .actions > a:hover { 285 | font-size: 14px; 286 | color: #1BA0FF; 287 | text-decoration: underline; 288 | } 289 | 290 | .actions > button { 291 | background: none; 292 | border: none; 293 | padding: 0; 294 | color: #1BA0FF; 295 | cursor: pointer; 296 | } 297 | 298 | .actions > button:hover { 299 | background: none; 300 | border: none; 301 | padding: 0; 302 | color: #1BA0FF; 303 | cursor: pointer; 304 | text-decoration: underline; 305 | } 306 | 307 | footer { 308 | text-align: center; 309 | color: #999; 310 | margin-bottom: 30px; 311 | } 312 | 313 | footer a { 314 | color: #111; 315 | text-decoration: none; 316 | font-size: 14px; 317 | } 318 | 319 | footer a:hover { 320 | color: #1BA0FF; 321 | } 322 | -------------------------------------------------------------------------------- /src/Bloggy/Pages/Themes/Dark/styles/bloggy.css: -------------------------------------------------------------------------------- 1 | /* Thin */ 2 | @font-face { 3 | font-family: "Roboto"; 4 | font-style: normal; 5 | font-weight: 100; 6 | src: url('../fonts/Roboto-Thin.ttf') format('truetype'); 7 | } 8 | 9 | @font-face { 10 | font-family: "Roboto"; 11 | font-style: italic; 12 | font-weight: 100; 13 | src: url('../fonts/Roboto-ThinItalic.ttf') format('truetype'); 14 | } 15 | 16 | /* Light */ 17 | @font-face { 18 | font-family: "Roboto"; 19 | font-style: normal; 20 | font-weight: 300; 21 | src: url('../fonts/Roboto-Light.ttf') format('truetype'); 22 | } 23 | 24 | @font-face { 25 | font-family: "Roboto"; 26 | font-style: italic; 27 | font-weight: 300; 28 | src: url('../fonts/Roboto-LightItalic.ttf') format('truetype'); 29 | } 30 | 31 | /* Normal */ 32 | @font-face { 33 | font-family: "Roboto"; 34 | font-style: normal; 35 | font-weight: 400; 36 | src: url('../fonts/Roboto-Regular.ttf') format('truetype'); 37 | } 38 | 39 | @font-face { 40 | font-family: "Roboto"; 41 | font-style: italic; 42 | font-weight: 400; 43 | src: url('../fonts/Roboto-Italic.ttf') format('truetype'); 44 | } 45 | 46 | /* Medium */ 47 | @font-face { 48 | font-family: "Roboto"; 49 | font-style: normal; 50 | font-weight: 500; 51 | src: url('../fonts/Roboto-Medium.ttf') format('truetype'); 52 | } 53 | 54 | @font-face { 55 | font-family: "Roboto"; 56 | font-style: italic; 57 | font-weight: 500; 58 | src: url('../fonts/Roboto-MediumItalic.ttf') format('truetype'); 59 | } 60 | 61 | /* Bold */ 62 | @font-face { 63 | font-family: "Roboto"; 64 | font-style: normal; 65 | font-weight: 700; 66 | src: url('../fonts/Roboto-Bold.ttf') format('truetype'); 67 | } 68 | 69 | @font-face { 70 | font-family: "Roboto"; 71 | font-style: italic; 72 | font-weight: 700; 73 | src: url('../fonts/Roboto-BoldItalic.ttf') format('truetype'); 74 | } 75 | 76 | /* Black */ 77 | @font-face { 78 | font-family: "Roboto"; 79 | font-style: normal; 80 | font-weight: 900; 81 | src: url('../fonts/Roboto-Black.ttf') format('truetype'); 82 | } 83 | 84 | @font-face { 85 | font-family: "Roboto"; 86 | font-style: italic; 87 | font-weight: 900; 88 | src: url('../fonts/Roboto-BlackItalic.ttf') format('truetype'); 89 | } 90 | 91 | body { 92 | background-color: #1f1f1f; 93 | font-family: 'Roboto', sans-serif; 94 | } 95 | 96 | .container { 97 | width: 670px; 98 | } 99 | 100 | 101 | header { 102 | margin: 30px auto; 103 | text-align: center; 104 | } 105 | 106 | header h1 { 107 | font-size: 60px; 108 | font-weight: lighter; 109 | color:#fff; 110 | } 111 | 112 | header nav { 113 | margin: auto; 114 | text-align: center; 115 | margin-top: 30px; 116 | margin-bottom: 10px; 117 | } 118 | 119 | header nav a { 120 | color: #fff; 121 | text-transform: uppercase; 122 | text-decoration: none; 123 | font-size: 18px; 124 | margin-right: 15px; 125 | } 126 | 127 | header nav a.active, a:hover { 128 | color: #1ba0ff; 129 | text-decoration: none; 130 | } 131 | 132 | header a { 133 | color: #1BA0FF; 134 | text-decoration: none; 135 | } 136 | 137 | header a:hover { 138 | color: #1ba0ff; 139 | text-decoration: none; 140 | } 141 | 142 | article { 143 | margin-bottom: 30px; 144 | } 145 | 146 | article a { 147 | color: #1ba0ff; 148 | font-size: 28px; 149 | font-weight: normal; 150 | outline: none; 151 | } 152 | 153 | article a:hover { 154 | color: #1BA0FF; 155 | text-decoration: underline; 156 | } 157 | 158 | article time { 159 | color: #999; 160 | } 161 | 162 | article span#tags > a { 163 | color: #1BA0FF; 164 | font-size: 16px; 165 | } 166 | 167 | article span#tags > a:hover { 168 | text-decoration: underline; 169 | } 170 | 171 | article p, time { 172 | color: #fff; 173 | font-size: 16px; 174 | line-height: 24px; 175 | } 176 | 177 | #comments { 178 | margin-left: -15px; 179 | } 180 | 181 | #comments h3 { 182 | color: #999; 183 | } 184 | 185 | .comment { 186 | margin-top: 20px; 187 | padding-bottom: 20px; 188 | border-bottom: 1px solid #ddd; 189 | } 190 | 191 | .comment:nth-last-child(1) { 192 | border-bottom: none; 193 | } 194 | 195 | .comment img { 196 | float: left; 197 | width: 100px; 198 | padding-right: 15px; 199 | } 200 | 201 | .comment div { 202 | overflow: auto; 203 | } 204 | 205 | .comment a { 206 | color: #1BA0FF; 207 | font-size: 20px; 208 | font-weight: normal; 209 | outline: none; 210 | } 211 | 212 | .comment a:hover { 213 | color: #1BA0FF; 214 | text-decoration: underline; 215 | } 216 | 217 | .comment time { 218 | color: #999; 219 | display: block; 220 | } 221 | 222 | .comment p, time { 223 | color: #fff; 224 | font-size: 16px; 225 | line-height: 24px; 226 | } 227 | 228 | #commentForm, .alert-success, .alert-error { 229 | margin-left: -15px; 230 | margin-top: 20px; 231 | } 232 | 233 | #commentForm label { 234 | color: #fff; 235 | } 236 | 237 | #commentForm span { 238 | color: #999; 239 | } 240 | 241 | #commentForm a { 242 | color: #1ba0ff !important; 243 | } 244 | 245 | #commentForm ul { 246 | margin-left: -25px; 247 | } 248 | 249 | #commentForm button[type=submit] { 250 | background-color: #1BA0FF; 251 | color: #fff; 252 | } 253 | 254 | #archive span.label, #tags span.label { 255 | background-color: #1BA0FF; 256 | } 257 | 258 | #archive span.label:hover, #tags span.label:hover { 259 | background-color: #999; 260 | } 261 | 262 | #archive h3, #tags h3 { 263 | color: #999; 264 | } 265 | 266 | #archive li > a, #tags li > a { 267 | font-size: 16px; 268 | color: #1BA0FF; 269 | text-decoration: underline; 270 | } 271 | 272 | #archive li > a:hover, #tags li > a:hover { 273 | color: #fff; 274 | } 275 | 276 | #contact h2, #about h2, #login h2 { 277 | color: #999; 278 | } 279 | 280 | #contact p, #about p { 281 | color: #fff; 282 | } 283 | 284 | #contact a { 285 | color: #1BA0FF; 286 | text-decoration: none; 287 | font-size: 14px; 288 | } 289 | 290 | #contact a:hover { 291 | color: #111; 292 | } 293 | 294 | #login label { 295 | color: #fff; 296 | } 297 | 298 | #login button[type=submit] { 299 | background-color: #1BA0FF; 300 | color: #fff; 301 | } 302 | 303 | #new-post { 304 | margin-left: -15px; 305 | } 306 | 307 | .actions { 308 | color : #fff; 309 | } 310 | 311 | .actions > a { 312 | font-size: 14px; 313 | color: #1BA0FF; 314 | } 315 | 316 | .actions > a:hover { 317 | font-size: 14px; 318 | color: #1BA0FF; 319 | text-decoration: underline; 320 | } 321 | 322 | .actions > button { 323 | background: none; 324 | border: none; 325 | padding: 0; 326 | color: #1BA0FF; 327 | cursor: pointer; 328 | } 329 | 330 | .actions > button:hover { 331 | background: none; 332 | border: none; 333 | padding: 0; 334 | color: #1BA0FF; 335 | cursor: pointer; 336 | text-decoration: underline; 337 | } 338 | 339 | footer { 340 | text-align: center; 341 | color: #999; 342 | margin-bottom: 30px; 343 | } 344 | 345 | footer a { 346 | color: #1BA0FF; 347 | text-decoration: none; 348 | font-size: 14px; 349 | } 350 | 351 | footer a:hover { 352 | color: #111; 353 | } 354 | -------------------------------------------------------------------------------- /src/Bloggy/wwwroot/styles/simplemde.css: -------------------------------------------------------------------------------- 1 | .CodeMirror { 2 | height: auto; 3 | min-height: 300px; 4 | border: 1px solid #ddd; 5 | border-bottom-left-radius: 4px; 6 | border-bottom-right-radius: 4px; 7 | padding: 10px; 8 | font: inherit; 9 | z-index: 1; 10 | } 11 | 12 | .CodeMirror-scroll { 13 | min-height: 300px 14 | } 15 | 16 | .CodeMirror-fullscreen { 17 | background: #fff; 18 | position: fixed !important; 19 | top: 50px; 20 | left: 0; 21 | right: 0; 22 | bottom: 0; 23 | height: auto; 24 | z-index: 9; 25 | } 26 | 27 | .CodeMirror-sided { 28 | width: 50% !important; 29 | } 30 | 31 | .editor-toolbar { 32 | position: relative; 33 | opacity: .6; 34 | -webkit-user-select: none; 35 | -moz-user-select: none; 36 | -ms-user-select: none; 37 | -o-user-select: none; 38 | user-select: none; 39 | padding: 0 10px; 40 | border-top: 1px solid #bbb; 41 | border-left: 1px solid #bbb; 42 | border-right: 1px solid #bbb; 43 | border-top-left-radius: 4px; 44 | border-top-right-radius: 4px; 45 | } 46 | 47 | .editor-toolbar:after, 48 | .editor-toolbar:before { 49 | display: block; 50 | content: ' '; 51 | height: 1px; 52 | } 53 | 54 | .editor-toolbar:before { 55 | margin-bottom: 8px 56 | } 57 | 58 | .editor-toolbar:after { 59 | margin-top: 8px 60 | } 61 | 62 | .editor-toolbar:hover, 63 | .editor-wrapper input.title:focus, 64 | .editor-wrapper input.title:hover { 65 | opacity: .8 66 | } 67 | 68 | .editor-toolbar.fullscreen { 69 | width: 100%; 70 | height: 50px; 71 | overflow-x: auto; 72 | overflow-y: hidden; 73 | white-space: nowrap; 74 | padding-top: 10px; 75 | padding-bottom: 10px; 76 | box-sizing: border-box; 77 | background: #fff; 78 | border: 0; 79 | position: fixed; 80 | top: 0; 81 | left: 0; 82 | opacity: 1; 83 | z-index: 9; 84 | } 85 | 86 | .editor-toolbar.fullscreen::before { 87 | width: 20px; 88 | height: 50px; 89 | background: -moz-linear-gradient(left, rgba(255, 255, 255, 1) 0%, rgba(255, 255, 255, 0) 100%); 90 | background: -webkit-gradient(linear, left top, right top, color-stop(0%, rgba(255, 255, 255, 1)), color-stop(100%, rgba(255, 255, 255, 0))); 91 | background: -webkit-linear-gradient(left, rgba(255, 255, 255, 1) 0%, rgba(255, 255, 255, 0) 100%); 92 | background: -o-linear-gradient(left, rgba(255, 255, 255, 1) 0%, rgba(255, 255, 255, 0) 100%); 93 | background: -ms-linear-gradient(left, rgba(255, 255, 255, 1) 0%, rgba(255, 255, 255, 0) 100%); 94 | background: linear-gradient(to right, rgba(255, 255, 255, 1) 0%, rgba(255, 255, 255, 0) 100%); 95 | position: fixed; 96 | top: 0; 97 | left: 0; 98 | margin: 0; 99 | padding: 0; 100 | } 101 | 102 | .editor-toolbar.fullscreen::after { 103 | width: 20px; 104 | height: 50px; 105 | background: -moz-linear-gradient(left, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 1) 100%); 106 | background: -webkit-gradient(linear, left top, right top, color-stop(0%, rgba(255, 255, 255, 0)), color-stop(100%, rgba(255, 255, 255, 1))); 107 | background: -webkit-linear-gradient(left, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 1) 100%); 108 | background: -o-linear-gradient(left, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 1) 100%); 109 | background: -ms-linear-gradient(left, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 1) 100%); 110 | background: linear-gradient(to right, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 1) 100%); 111 | position: fixed; 112 | top: 0; 113 | right: 0; 114 | margin: 0; 115 | padding: 0; 116 | } 117 | 118 | .editor-toolbar a { 119 | display: inline-block; 120 | text-align: center; 121 | text-decoration: none!important; 122 | color: #2c3e50!important; 123 | width: 30px; 124 | height: 30px; 125 | margin: 0; 126 | border: 1px solid transparent; 127 | border-radius: 3px; 128 | cursor: pointer; 129 | } 130 | 131 | .editor-toolbar a.active, 132 | .editor-toolbar a:hover { 133 | background: #fcfcfc; 134 | border-color: #95a5a6; 135 | } 136 | 137 | .editor-toolbar a:before { 138 | line-height: 30px 139 | } 140 | 141 | .editor-toolbar i.separator { 142 | display: inline-block; 143 | width: 0; 144 | border-left: 1px solid #d9d9d9; 145 | border-right: 1px solid #fff; 146 | color: transparent; 147 | text-indent: -10px; 148 | margin: 0 6px; 149 | } 150 | 151 | .editor-toolbar a.fa-header-x:after { 152 | font-family: Arial, "Helvetica Neue", Helvetica, sans-serif; 153 | font-size: 65%; 154 | vertical-align: text-bottom; 155 | position: relative; 156 | top: 2px; 157 | } 158 | 159 | .editor-toolbar a.fa-header-1:after { 160 | content: "1"; 161 | } 162 | 163 | .editor-toolbar a.fa-header-2:after { 164 | content: "2"; 165 | } 166 | 167 | .editor-toolbar a.fa-header-3:after { 168 | content: "3"; 169 | } 170 | 171 | .editor-toolbar a.fa-header-bigger:after { 172 | content: "▲"; 173 | } 174 | 175 | .editor-toolbar a.fa-header-smaller:after { 176 | content: "▼"; 177 | } 178 | 179 | .editor-toolbar.disabled-for-preview a:not(.no-disable) { 180 | pointer-events: none; 181 | background: #fff; 182 | border-color: transparent; 183 | text-shadow: inherit; 184 | } 185 | 186 | @media only screen and (max-width: 700px) { 187 | .editor-toolbar a.no-mobile { 188 | display: none; 189 | } 190 | } 191 | 192 | .editor-statusbar { 193 | padding: 8px 10px; 194 | font-size: 12px; 195 | color: #959694; 196 | text-align: right; 197 | } 198 | 199 | .editor-statusbar span { 200 | display: inline-block; 201 | min-width: 4em; 202 | margin-left: 1em; 203 | } 204 | 205 | .editor-statusbar .lines:before { 206 | content: 'lines: ' 207 | } 208 | 209 | .editor-statusbar .words:before { 210 | content: 'words: ' 211 | } 212 | 213 | .editor-statusbar .characters:before { 214 | content: 'characters: ' 215 | } 216 | 217 | .editor-preview { 218 | padding: 10px; 219 | position: absolute; 220 | width: 100%; 221 | height: 100%; 222 | top: 0; 223 | left: 0; 224 | background: #fafafa; 225 | z-index: 7; 226 | overflow: auto; 227 | display: none; 228 | box-sizing: border-box; 229 | } 230 | 231 | .editor-preview-side { 232 | padding: 10px; 233 | position: fixed; 234 | bottom: 0; 235 | width: 50%; 236 | top: 50px; 237 | right: 0; 238 | background: #fafafa; 239 | z-index: 9; 240 | overflow: auto; 241 | display: none; 242 | box-sizing: border-box; 243 | border: 1px solid #ddd; 244 | } 245 | 246 | .editor-preview-active-side { 247 | display: block 248 | } 249 | 250 | .editor-preview-active { 251 | display: block 252 | } 253 | 254 | .editor-preview>p, 255 | .editor-preview-side>p { 256 | margin-top: 0 257 | } 258 | 259 | .editor-preview pre, 260 | .editor-preview-side pre { 261 | background: #eee; 262 | margin-bottom: 10px; 263 | } 264 | 265 | .editor-preview table td, 266 | .editor-preview table th, 267 | .editor-preview-side table td, 268 | .editor-preview-side table th { 269 | border: 1px solid #ddd; 270 | padding: 5px; 271 | } 272 | 273 | .CodeMirror .CodeMirror-code .cm-tag { 274 | color: #63a35c; 275 | } 276 | 277 | .CodeMirror .CodeMirror-code .cm-attribute { 278 | color: #795da3; 279 | } 280 | 281 | .CodeMirror .CodeMirror-code .cm-string { 282 | color: #183691; 283 | } 284 | 285 | .CodeMirror .CodeMirror-selected { 286 | background: #d9d9d9; 287 | } 288 | 289 | .CodeMirror .CodeMirror-code .cm-header-1 { 290 | font-size: 200%; 291 | line-height: 200%; 292 | } 293 | 294 | .CodeMirror .CodeMirror-code .cm-header-2 { 295 | font-size: 160%; 296 | line-height: 160%; 297 | } 298 | 299 | .CodeMirror .CodeMirror-code .cm-header-3 { 300 | font-size: 125%; 301 | line-height: 125%; 302 | } 303 | 304 | .CodeMirror .CodeMirror-code .cm-header-4 { 305 | font-size: 110%; 306 | line-height: 110%; 307 | } 308 | 309 | .CodeMirror .CodeMirror-code .cm-comment { 310 | background: rgba(0, 0, 0, .05); 311 | border-radius: 2px; 312 | } 313 | 314 | .CodeMirror .CodeMirror-code .cm-link { 315 | color: #7f8c8d; 316 | } 317 | 318 | .CodeMirror .CodeMirror-code .cm-url { 319 | color: #aab2b3; 320 | } 321 | 322 | .CodeMirror .CodeMirror-code .cm-strikethrough { 323 | text-decoration: line-through; 324 | } 325 | 326 | .CodeMirror .CodeMirror-placeholder { 327 | opacity: .5; 328 | } -------------------------------------------------------------------------------- /src/Bloggy/wwwroot/styles/simplemde.min.css: -------------------------------------------------------------------------------- 1 | /** 2 | * simplemde v1.11.2 3 | * Copyright Next Step Webs, Inc. 4 | * @link https://github.com/NextStepWebs/simplemde-markdown-editor 5 | * @license MIT 6 | */ 7 | .CodeMirror{color:#000}.CodeMirror-lines{padding:4px 0}.CodeMirror pre{padding:0 4px}.CodeMirror-gutter-filler,.CodeMirror-scrollbar-filler{background-color:#fff}.CodeMirror-gutters{border-right:1px solid #ddd;background-color:#f7f7f7;white-space:nowrap}.CodeMirror-linenumber{padding:0 3px 0 5px;min-width:20px;text-align:right;color:#999;white-space:nowrap}.CodeMirror-guttermarker{color:#000}.CodeMirror-guttermarker-subtle{color:#999}.CodeMirror-cursor{border-left:1px solid #000;border-right:none;width:0}.CodeMirror div.CodeMirror-secondarycursor{border-left:1px solid silver}.cm-fat-cursor .CodeMirror-cursor{width:auto;border:0!important;background:#7e7}.cm-fat-cursor div.CodeMirror-cursors{z-index:1}.cm-animate-fat-cursor{width:auto;border:0;-webkit-animation:blink 1.06s steps(1) infinite;-moz-animation:blink 1.06s steps(1) infinite;animation:blink 1.06s steps(1) infinite;background-color:#7e7}@-moz-keyframes blink{50%{background-color:transparent}}@-webkit-keyframes blink{50%{background-color:transparent}}@keyframes blink{50%{background-color:transparent}}.cm-tab{display:inline-block;text-decoration:inherit}.CodeMirror-ruler{border-left:1px solid #ccc;position:absolute}.cm-s-default .cm-header{color:#00f}.cm-s-default .cm-quote{color:#090}.cm-negative{color:#d44}.cm-positive{color:#292}.cm-header,.cm-strong{font-weight:700}.cm-em{font-style:italic}.cm-link{text-decoration:underline}.cm-strikethrough{text-decoration:line-through}.cm-s-default .cm-keyword{color:#708}.cm-s-default .cm-atom{color:#219}.cm-s-default .cm-number{color:#164}.cm-s-default .cm-def{color:#00f}.cm-s-default .cm-variable-2{color:#05a}.cm-s-default .cm-variable-3{color:#085}.cm-s-default .cm-comment{color:#a50}.cm-s-default .cm-string{color:#a11}.cm-s-default .cm-string-2{color:#f50}.cm-s-default .cm-meta,.cm-s-default .cm-qualifier{color:#555}.cm-s-default .cm-builtin{color:#30a}.cm-s-default .cm-bracket{color:#997}.cm-s-default .cm-tag{color:#170}.cm-s-default .cm-attribute{color:#00c}.cm-s-default .cm-hr{color:#999}.cm-s-default .cm-link{color:#00c}.cm-invalidchar,.cm-s-default .cm-error{color:red}.CodeMirror-composing{border-bottom:2px solid}div.CodeMirror span.CodeMirror-matchingbracket{color:#0f0}div.CodeMirror span.CodeMirror-nonmatchingbracket{color:#f22}.CodeMirror-matchingtag{background:rgba(255,150,0,.3)}.CodeMirror-activeline-background{background:#e8f2ff}.CodeMirror{position:relative;overflow:hidden;background:#fff}.CodeMirror-scroll{overflow:scroll!important;margin-bottom:-30px;margin-right:-30px;padding-bottom:30px;height:100%;outline:0;position:relative}.CodeMirror-sizer{position:relative;border-right:30px solid transparent}.CodeMirror-gutter-filler,.CodeMirror-hscrollbar,.CodeMirror-scrollbar-filler,.CodeMirror-vscrollbar{position:absolute;z-index:6;display:none}.CodeMirror-vscrollbar{right:0;top:0;overflow-x:hidden;overflow-y:scroll}.CodeMirror-hscrollbar{bottom:0;left:0;overflow-y:hidden;overflow-x:scroll}.CodeMirror-scrollbar-filler{right:0;bottom:0}.CodeMirror-gutter-filler{left:0;bottom:0}.CodeMirror-gutters{position:absolute;left:0;top:0;min-height:100%;z-index:3}.CodeMirror-gutter{white-space:normal;height:100%;display:inline-block;vertical-align:top;margin-bottom:-30px}.CodeMirror-gutter-wrapper{position:absolute;z-index:4;background:0 0!important;border:none!important;-webkit-user-select:none;-moz-user-select:none;user-select:none}.CodeMirror-gutter-background{position:absolute;top:0;bottom:0;z-index:4}.CodeMirror-gutter-elt{position:absolute;cursor:default;z-index:4}.CodeMirror-lines{cursor:text;min-height:1px}.CodeMirror pre{-moz-border-radius:0;-webkit-border-radius:0;border-radius:0;border-width:0;background:0 0;font-family:inherit;font-size:inherit;margin:0;white-space:pre;word-wrap:normal;line-height:inherit;color:inherit;z-index:2;position:relative;overflow:visible;-webkit-tap-highlight-color:transparent;-webkit-font-variant-ligatures:none;font-variant-ligatures:none}.CodeMirror-wrap pre{word-wrap:break-word;white-space:pre-wrap;word-break:normal}.CodeMirror-linebackground{position:absolute;left:0;right:0;top:0;bottom:0;z-index:0}.CodeMirror-linewidget{position:relative;z-index:2;overflow:auto}.CodeMirror-code{outline:0}.CodeMirror-gutter,.CodeMirror-gutters,.CodeMirror-linenumber,.CodeMirror-scroll,.CodeMirror-sizer{-moz-box-sizing:content-box;box-sizing:content-box}.CodeMirror-measure{position:absolute;width:100%;height:0;overflow:hidden;visibility:hidden}.CodeMirror-cursor{position:absolute}.CodeMirror-measure pre{position:static}div.CodeMirror-cursors{visibility:hidden;position:relative;z-index:3}.CodeMirror-focused div.CodeMirror-cursors,div.CodeMirror-dragcursors{visibility:visible}.CodeMirror-selected{background:#d9d9d9}.CodeMirror-focused .CodeMirror-selected,.CodeMirror-line::selection,.CodeMirror-line>span::selection,.CodeMirror-line>span>span::selection{background:#d7d4f0}.CodeMirror-crosshair{cursor:crosshair}.CodeMirror-line::-moz-selection,.CodeMirror-line>span::-moz-selection,.CodeMirror-line>span>span::-moz-selection{background:#d7d4f0}.cm-searching{background:#ffa;background:rgba(255,255,0,.4)}.cm-force-border{padding-right:.1px}@media print{.CodeMirror div.CodeMirror-cursors{visibility:hidden}}.cm-tab-wrap-hack:after{content:''}span.CodeMirror-selectedtext{background:0 0}.CodeMirror{height:auto;min-height:300px;border:1px solid #ddd;border-bottom-left-radius:4px;border-bottom-right-radius:4px;padding:10px;font:inherit;z-index:1}.CodeMirror-scroll{min-height:300px}.CodeMirror-fullscreen{background:#fff;position:fixed!important;top:50px;left:0;right:0;bottom:0;height:auto;z-index:9}.CodeMirror-sided{width:50%!important}.editor-toolbar{position:relative;opacity:.6;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;-o-user-select:none;user-select:none;padding:0 10px;border-top:1px solid #bbb;border-left:1px solid #bbb;border-right:1px solid #bbb;border-top-left-radius:4px;border-top-right-radius:4px}.editor-toolbar:after,.editor-toolbar:before{display:block;content:' ';height:1px}.editor-toolbar:before{margin-bottom:8px}.editor-toolbar:after{margin-top:8px}.editor-toolbar:hover,.editor-wrapper input.title:focus,.editor-wrapper input.title:hover{opacity:.8}.editor-toolbar.fullscreen{width:100%;height:50px;overflow-x:auto;overflow-y:hidden;white-space:nowrap;padding-top:10px;padding-bottom:10px;box-sizing:border-box;background:#fff;border:0;position:fixed;top:0;left:0;opacity:1;z-index:9}.editor-toolbar.fullscreen::before{width:20px;height:50px;background:-moz-linear-gradient(left,rgba(255,255,255,1) 0,rgba(255,255,255,0) 100%);background:-webkit-gradient(linear,left top,right top,color-stop(0,rgba(255,255,255,1)),color-stop(100%,rgba(255,255,255,0)));background:-webkit-linear-gradient(left,rgba(255,255,255,1) 0,rgba(255,255,255,0) 100%);background:-o-linear-gradient(left,rgba(255,255,255,1) 0,rgba(255,255,255,0) 100%);background:-ms-linear-gradient(left,rgba(255,255,255,1) 0,rgba(255,255,255,0) 100%);background:linear-gradient(to right,rgba(255,255,255,1) 0,rgba(255,255,255,0) 100%);position:fixed;top:0;left:0;margin:0;padding:0}.editor-toolbar.fullscreen::after{width:20px;height:50px;background:-moz-linear-gradient(left,rgba(255,255,255,0) 0,rgba(255,255,255,1) 100%);background:-webkit-gradient(linear,left top,right top,color-stop(0,rgba(255,255,255,0)),color-stop(100%,rgba(255,255,255,1)));background:-webkit-linear-gradient(left,rgba(255,255,255,0) 0,rgba(255,255,255,1) 100%);background:-o-linear-gradient(left,rgba(255,255,255,0) 0,rgba(255,255,255,1) 100%);background:-ms-linear-gradient(left,rgba(255,255,255,0) 0,rgba(255,255,255,1) 100%);background:linear-gradient(to right,rgba(255,255,255,0) 0,rgba(255,255,255,1) 100%);position:fixed;top:0;right:0;margin:0;padding:0}.editor-toolbar a{display:inline-block;text-align:center;text-decoration:none!important;color:#2c3e50!important;width:30px;height:30px;margin:0;border:1px solid transparent;border-radius:3px;cursor:pointer}.editor-toolbar a.active,.editor-toolbar a:hover{background:#fcfcfc;border-color:#95a5a6}.editor-toolbar a:before{line-height:30px}.editor-toolbar i.separator{display:inline-block;width:0;border-left:1px solid #d9d9d9;border-right:1px solid #fff;color:transparent;text-indent:-10px;margin:0 6px}.editor-toolbar a.fa-header-x:after{font-family:Arial,"Helvetica Neue",Helvetica,sans-serif;font-size:65%;vertical-align:text-bottom;position:relative;top:2px}.editor-toolbar a.fa-header-1:after{content:"1"}.editor-toolbar a.fa-header-2:after{content:"2"}.editor-toolbar a.fa-header-3:after{content:"3"}.editor-toolbar a.fa-header-bigger:after{content:"▲"}.editor-toolbar a.fa-header-smaller:after{content:"▼"}.editor-toolbar.disabled-for-preview a:not(.no-disable){pointer-events:none;background:#fff;border-color:transparent;text-shadow:inherit}@media only screen and (max-width:700px){.editor-toolbar a.no-mobile{display:none}}.editor-statusbar{padding:8px 10px;font-size:12px;color:#959694;text-align:right}.editor-statusbar span{display:inline-block;min-width:4em;margin-left:1em}.editor-preview,.editor-preview-side{padding:10px;background:#fafafa;overflow:auto;display:none;box-sizing:border-box}.editor-statusbar .lines:before{content:'lines: '}.editor-statusbar .words:before{content:'words: '}.editor-statusbar .characters:before{content:'characters: '}.editor-preview{position:absolute;width:100%;height:100%;top:0;left:0;z-index:7}.editor-preview-side{position:fixed;bottom:0;width:50%;top:50px;right:0;z-index:9;border:1px solid #ddd}.editor-preview-active,.editor-preview-active-side{display:block}.editor-preview-side>p,.editor-preview>p{margin-top:0}.editor-preview pre,.editor-preview-side pre{background:#eee;margin-bottom:10px}.editor-preview table td,.editor-preview table th,.editor-preview-side table td,.editor-preview-side table th{border:1px solid #ddd;padding:5px}.CodeMirror .CodeMirror-code .cm-tag{color:#63a35c}.CodeMirror .CodeMirror-code .cm-attribute{color:#795da3}.CodeMirror .CodeMirror-code .cm-string{color:#183691}.CodeMirror .CodeMirror-selected{background:#d9d9d9}.CodeMirror .CodeMirror-code .cm-header-1{font-size:200%;line-height:200%}.CodeMirror .CodeMirror-code .cm-header-2{font-size:160%;line-height:160%}.CodeMirror .CodeMirror-code .cm-header-3{font-size:125%;line-height:125%}.CodeMirror .CodeMirror-code .cm-header-4{font-size:110%;line-height:110%}.CodeMirror .CodeMirror-code .cm-comment{background:rgba(0,0,0,.05);border-radius:2px}.CodeMirror .CodeMirror-code .cm-link{color:#7f8c8d}.CodeMirror .CodeMirror-code .cm-url{color:#aab2b3}.CodeMirror .CodeMirror-code .cm-strikethrough{text-decoration:line-through}.CodeMirror .CodeMirror-placeholder{opacity:.5}.CodeMirror .cm-spell-error:not(.cm-url):not(.cm-comment):not(.cm-tag):not(.cm-word){background:rgba(255,0,0,.15)} -------------------------------------------------------------------------------- /src/Bloggy/wwwroot/scripts/simplemde.js: -------------------------------------------------------------------------------- 1 | /*global require,module*/ 2 | "use strict"; 3 | var CodeMirror = require("codemirror"); 4 | require("codemirror/addon/edit/continuelist.js"); 5 | require("./codemirror/tablist"); 6 | require("codemirror/addon/display/fullscreen.js"); 7 | require("codemirror/mode/markdown/markdown.js"); 8 | require("codemirror/addon/mode/overlay.js"); 9 | require("codemirror/addon/display/placeholder.js"); 10 | require("codemirror/addon/selection/mark-selection.js"); 11 | require("codemirror/mode/gfm/gfm.js"); 12 | require("codemirror/mode/xml/xml.js"); 13 | var CodeMirrorSpellChecker = require("codemirror-spell-checker"); 14 | var marked = require("marked"); 15 | 16 | 17 | // Some variables 18 | var isMac = /Mac/.test(navigator.platform); 19 | 20 | // Mapping of actions that can be bound to keyboard shortcuts or toolbar buttons 21 | var bindings = { 22 | "toggleBold": toggleBold, 23 | "toggleItalic": toggleItalic, 24 | "drawLink": drawLink, 25 | "toggleHeadingSmaller": toggleHeadingSmaller, 26 | "toggleHeadingBigger": toggleHeadingBigger, 27 | "drawImage": drawImage, 28 | "toggleBlockquote": toggleBlockquote, 29 | "toggleOrderedList": toggleOrderedList, 30 | "toggleUnorderedList": toggleUnorderedList, 31 | "toggleCodeBlock": toggleCodeBlock, 32 | "togglePreview": togglePreview, 33 | "toggleStrikethrough": toggleStrikethrough, 34 | "toggleHeading1": toggleHeading1, 35 | "toggleHeading2": toggleHeading2, 36 | "toggleHeading3": toggleHeading3, 37 | "cleanBlock": cleanBlock, 38 | "drawTable": drawTable, 39 | "drawHorizontalRule": drawHorizontalRule, 40 | "undo": undo, 41 | "redo": redo, 42 | "toggleSideBySide": toggleSideBySide, 43 | "toggleFullScreen": toggleFullScreen 44 | }; 45 | 46 | var shortcuts = { 47 | "toggleBold": "Cmd-B", 48 | "toggleItalic": "Cmd-I", 49 | "drawLink": "Cmd-K", 50 | "toggleHeadingSmaller": "Cmd-H", 51 | "toggleHeadingBigger": "Shift-Cmd-H", 52 | "cleanBlock": "Cmd-E", 53 | "drawImage": "Cmd-Alt-I", 54 | "toggleBlockquote": "Cmd-'", 55 | "toggleOrderedList": "Cmd-Alt-L", 56 | "toggleUnorderedList": "Cmd-L", 57 | "toggleCodeBlock": "Cmd-Alt-C", 58 | "togglePreview": "Cmd-P", 59 | "toggleSideBySide": "F9", 60 | "toggleFullScreen": "F11" 61 | }; 62 | 63 | var getBindingName = function(f) { 64 | for(var key in bindings) { 65 | if(bindings[key] === f) { 66 | return key; 67 | } 68 | } 69 | return null; 70 | }; 71 | 72 | var isMobile = function() { 73 | var check = false; 74 | (function(a) { 75 | if(/(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|android|ipad|playbook|silk/i.test(a) || /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(a.substr(0, 4))) check = true; 76 | })(navigator.userAgent || navigator.vendor || window.opera); 77 | return check; 78 | }; 79 | 80 | 81 | /** 82 | * Fix shortcut. Mac use Command, others use Ctrl. 83 | */ 84 | function fixShortcut(name) { 85 | if(isMac) { 86 | name = name.replace("Ctrl", "Cmd"); 87 | } else { 88 | name = name.replace("Cmd", "Ctrl"); 89 | } 90 | return name; 91 | } 92 | 93 | 94 | /** 95 | * Create icon element for toolbar. 96 | */ 97 | function createIcon(options, enableTooltips, shortcuts) { 98 | options = options || {}; 99 | var el = document.createElement("a"); 100 | enableTooltips = (enableTooltips == undefined) ? true : enableTooltips; 101 | 102 | if(options.title && enableTooltips) { 103 | el.title = createTootlip(options.title, options.action, shortcuts); 104 | 105 | if(isMac) { 106 | el.title = el.title.replace("Ctrl", "⌘"); 107 | el.title = el.title.replace("Alt", "⌥"); 108 | } 109 | } 110 | 111 | el.tabIndex = -1; 112 | el.className = options.className; 113 | return el; 114 | } 115 | 116 | function createSep() { 117 | var el = document.createElement("i"); 118 | el.className = "separator"; 119 | el.innerHTML = "|"; 120 | return el; 121 | } 122 | 123 | function createTootlip(title, action, shortcuts) { 124 | var actionName; 125 | var tooltip = title; 126 | 127 | if(action) { 128 | actionName = getBindingName(action); 129 | if(shortcuts[actionName]) { 130 | tooltip += " (" + fixShortcut(shortcuts[actionName]) + ")"; 131 | } 132 | } 133 | 134 | return tooltip; 135 | } 136 | 137 | /** 138 | * The state of CodeMirror at the given position. 139 | */ 140 | function getState(cm, pos) { 141 | pos = pos || cm.getCursor("start"); 142 | var stat = cm.getTokenAt(pos); 143 | if(!stat.type) return {}; 144 | 145 | var types = stat.type.split(" "); 146 | 147 | var ret = {}, 148 | data, text; 149 | for(var i = 0; i < types.length; i++) { 150 | data = types[i]; 151 | if(data === "strong") { 152 | ret.bold = true; 153 | } else if(data === "variable-2") { 154 | text = cm.getLine(pos.line); 155 | if(/^\s*\d+\.\s/.test(text)) { 156 | ret["ordered-list"] = true; 157 | } else { 158 | ret["unordered-list"] = true; 159 | } 160 | } else if(data === "atom") { 161 | ret.quote = true; 162 | } else if(data === "em") { 163 | ret.italic = true; 164 | } else if(data === "quote") { 165 | ret.quote = true; 166 | } else if(data === "strikethrough") { 167 | ret.strikethrough = true; 168 | } else if(data === "comment") { 169 | ret.code = true; 170 | } else if(data === "link") { 171 | ret.link = true; 172 | } else if(data === "tag") { 173 | ret.image = true; 174 | } else if(data.match(/^header(\-[1-6])?$/)) { 175 | ret[data.replace("header", "heading")] = true; 176 | } 177 | } 178 | return ret; 179 | } 180 | 181 | 182 | // Saved overflow setting 183 | var saved_overflow = ""; 184 | 185 | /** 186 | * Toggle full screen of the editor. 187 | */ 188 | function toggleFullScreen(editor) { 189 | // Set fullscreen 190 | var cm = editor.codemirror; 191 | cm.setOption("fullScreen", !cm.getOption("fullScreen")); 192 | 193 | 194 | // Prevent scrolling on body during fullscreen active 195 | if(cm.getOption("fullScreen")) { 196 | saved_overflow = document.body.style.overflow; 197 | document.body.style.overflow = "hidden"; 198 | } else { 199 | document.body.style.overflow = saved_overflow; 200 | } 201 | 202 | 203 | // Update toolbar class 204 | var wrap = cm.getWrapperElement(); 205 | 206 | if(!/fullscreen/.test(wrap.previousSibling.className)) { 207 | wrap.previousSibling.className += " fullscreen"; 208 | } else { 209 | wrap.previousSibling.className = wrap.previousSibling.className.replace(/\s*fullscreen\b/, ""); 210 | } 211 | 212 | 213 | // Update toolbar button 214 | var toolbarButton = editor.toolbarElements.fullscreen; 215 | 216 | if(!/active/.test(toolbarButton.className)) { 217 | toolbarButton.className += " active"; 218 | } else { 219 | toolbarButton.className = toolbarButton.className.replace(/\s*active\s*/g, ""); 220 | } 221 | 222 | 223 | // Hide side by side if needed 224 | var sidebyside = cm.getWrapperElement().nextSibling; 225 | if(/editor-preview-active-side/.test(sidebyside.className)) 226 | toggleSideBySide(editor); 227 | } 228 | 229 | 230 | /** 231 | * Action for toggling bold. 232 | */ 233 | function toggleBold(editor) { 234 | _toggleBlock(editor, "bold", editor.options.blockStyles.bold); 235 | } 236 | 237 | 238 | /** 239 | * Action for toggling italic. 240 | */ 241 | function toggleItalic(editor) { 242 | _toggleBlock(editor, "italic", editor.options.blockStyles.italic); 243 | } 244 | 245 | 246 | /** 247 | * Action for toggling strikethrough. 248 | */ 249 | function toggleStrikethrough(editor) { 250 | _toggleBlock(editor, "strikethrough", "~~"); 251 | } 252 | 253 | /** 254 | * Action for toggling code block. 255 | */ 256 | function toggleCodeBlock(editor) { 257 | var fenceCharsToInsert = editor.options.blockStyles.code; 258 | 259 | function fencing_line(line) { 260 | /* return true, if this is a ``` or ~~~ line */ 261 | if(typeof line !== "object") { 262 | throw "fencing_line() takes a 'line' object (not a line number, or line text). Got: " + typeof line + ": " + line; 263 | } 264 | return line.styles && line.styles[2] && line.styles[2].indexOf("formatting-code-block") !== -1; 265 | } 266 | 267 | function token_state(token) { 268 | // base goes an extra level deep when mode backdrops are used, e.g. spellchecker on 269 | return token.state.base.base || token.state.base; 270 | } 271 | 272 | function code_type(cm, line_num, line, firstTok, lastTok) { 273 | /* 274 | * Return "single", "indented", "fenced" or false 275 | * 276 | * cm and line_num are required. Others are optional for efficiency 277 | * To check in the middle of a line, pass in firstTok yourself. 278 | */ 279 | line = line || cm.getLineHandle(line_num); 280 | firstTok = firstTok || cm.getTokenAt({ 281 | line: line_num, 282 | ch: 1 283 | }); 284 | lastTok = lastTok || (!!line.text && cm.getTokenAt({ 285 | line: line_num, 286 | ch: line.text.length - 1 287 | })); 288 | var types = firstTok.type ? firstTok.type.split(" ") : []; 289 | if(lastTok && token_state(lastTok).indentedCode) { 290 | // have to check last char, since first chars of first line aren"t marked as indented 291 | return "indented"; 292 | } else if(types.indexOf("comment") === -1) { 293 | // has to be after "indented" check, since first chars of first indented line aren"t marked as such 294 | return false; 295 | } else if(token_state(firstTok).fencedChars || token_state(lastTok).fencedChars || fencing_line(line)) { 296 | return "fenced"; 297 | } else { 298 | return "single"; 299 | } 300 | } 301 | 302 | function insertFencingAtSelection(cm, cur_start, cur_end, fenceCharsToInsert) { 303 | var start_line_sel = cur_start.line + 1, 304 | end_line_sel = cur_end.line + 1, 305 | sel_multi = cur_start.line !== cur_end.line, 306 | repl_start = fenceCharsToInsert + "\n", 307 | repl_end = "\n" + fenceCharsToInsert; 308 | if(sel_multi) { 309 | end_line_sel++; 310 | } 311 | // handle last char including \n or not 312 | if(sel_multi && cur_end.ch === 0) { 313 | repl_end = fenceCharsToInsert + "\n"; 314 | end_line_sel--; 315 | } 316 | _replaceSelection(cm, false, [repl_start, repl_end]); 317 | cm.setSelection({ 318 | line: start_line_sel, 319 | ch: 0 320 | }, { 321 | line: end_line_sel, 322 | ch: 0 323 | }); 324 | } 325 | 326 | var cm = editor.codemirror, 327 | cur_start = cm.getCursor("start"), 328 | cur_end = cm.getCursor("end"), 329 | tok = cm.getTokenAt({ 330 | line: cur_start.line, 331 | ch: cur_start.ch || 1 332 | }), // avoid ch 0 which is a cursor pos but not token 333 | line = cm.getLineHandle(cur_start.line), 334 | is_code = code_type(cm, cur_start.line, line, tok); 335 | var block_start, block_end, lineCount; 336 | 337 | if(is_code === "single") { 338 | // similar to some SimpleMDE _toggleBlock logic 339 | var start = line.text.slice(0, cur_start.ch).replace("`", ""), 340 | end = line.text.slice(cur_start.ch).replace("`", ""); 341 | cm.replaceRange(start + end, { 342 | line: cur_start.line, 343 | ch: 0 344 | }, { 345 | line: cur_start.line, 346 | ch: 99999999999999 347 | }); 348 | cur_start.ch--; 349 | if(cur_start !== cur_end) { 350 | cur_end.ch--; 351 | } 352 | cm.setSelection(cur_start, cur_end); 353 | cm.focus(); 354 | } else if(is_code === "fenced") { 355 | if(cur_start.line !== cur_end.line || cur_start.ch !== cur_end.ch) { 356 | // use selection 357 | 358 | // find the fenced line so we know what type it is (tilde, backticks, number of them) 359 | for(block_start = cur_start.line; block_start >= 0; block_start--) { 360 | line = cm.getLineHandle(block_start); 361 | if(fencing_line(line)) { 362 | break; 363 | } 364 | } 365 | var fencedTok = cm.getTokenAt({ 366 | line: block_start, 367 | ch: 1 368 | }); 369 | var fence_chars = token_state(fencedTok).fencedChars; 370 | var start_text, start_line; 371 | var end_text, end_line; 372 | // check for selection going up against fenced lines, in which case we don't want to add more fencing 373 | if(fencing_line(cm.getLineHandle(cur_start.line))) { 374 | start_text = ""; 375 | start_line = cur_start.line; 376 | } else if(fencing_line(cm.getLineHandle(cur_start.line - 1))) { 377 | start_text = ""; 378 | start_line = cur_start.line - 1; 379 | } else { 380 | start_text = fence_chars + "\n"; 381 | start_line = cur_start.line; 382 | } 383 | if(fencing_line(cm.getLineHandle(cur_end.line))) { 384 | end_text = ""; 385 | end_line = cur_end.line; 386 | if(cur_end.ch === 0) { 387 | end_line += 1; 388 | } 389 | } else if(cur_end.ch !== 0 && fencing_line(cm.getLineHandle(cur_end.line + 1))) { 390 | end_text = ""; 391 | end_line = cur_end.line + 1; 392 | } else { 393 | end_text = fence_chars + "\n"; 394 | end_line = cur_end.line + 1; 395 | } 396 | if(cur_end.ch === 0) { 397 | // full last line selected, putting cursor at beginning of next 398 | end_line -= 1; 399 | } 400 | cm.operation(function() { 401 | // end line first, so that line numbers don't change 402 | cm.replaceRange(end_text, { 403 | line: end_line, 404 | ch: 0 405 | }, { 406 | line: end_line + (end_text ? 0 : 1), 407 | ch: 0 408 | }); 409 | cm.replaceRange(start_text, { 410 | line: start_line, 411 | ch: 0 412 | }, { 413 | line: start_line + (start_text ? 0 : 1), 414 | ch: 0 415 | }); 416 | }); 417 | cm.setSelection({ 418 | line: start_line + (start_text ? 1 : 0), 419 | ch: 0 420 | }, { 421 | line: end_line + (start_text ? 1 : -1), 422 | ch: 0 423 | }); 424 | cm.focus(); 425 | } else { 426 | // no selection, search for ends of this fenced block 427 | var search_from = cur_start.line; 428 | if(fencing_line(cm.getLineHandle(cur_start.line))) { // gets a little tricky if cursor is right on a fenced line 429 | if(code_type(cm, cur_start.line + 1) === "fenced") { 430 | block_start = cur_start.line; 431 | search_from = cur_start.line + 1; // for searching for "end" 432 | } else { 433 | block_end = cur_start.line; 434 | search_from = cur_start.line - 1; // for searching for "start" 435 | } 436 | } 437 | if(block_start === undefined) { 438 | for(block_start = search_from; block_start >= 0; block_start--) { 439 | line = cm.getLineHandle(block_start); 440 | if(fencing_line(line)) { 441 | break; 442 | } 443 | } 444 | } 445 | if(block_end === undefined) { 446 | lineCount = cm.lineCount(); 447 | for(block_end = search_from; block_end < lineCount; block_end++) { 448 | line = cm.getLineHandle(block_end); 449 | if(fencing_line(line)) { 450 | break; 451 | } 452 | } 453 | } 454 | cm.operation(function() { 455 | cm.replaceRange("", { 456 | line: block_start, 457 | ch: 0 458 | }, { 459 | line: block_start + 1, 460 | ch: 0 461 | }); 462 | cm.replaceRange("", { 463 | line: block_end - 1, 464 | ch: 0 465 | }, { 466 | line: block_end, 467 | ch: 0 468 | }); 469 | }); 470 | cm.focus(); 471 | } 472 | } else if(is_code === "indented") { 473 | if(cur_start.line !== cur_end.line || cur_start.ch !== cur_end.ch) { 474 | // use selection 475 | block_start = cur_start.line; 476 | block_end = cur_end.line; 477 | if(cur_end.ch === 0) { 478 | block_end--; 479 | } 480 | } else { 481 | // no selection, search for ends of this indented block 482 | for(block_start = cur_start.line; block_start >= 0; block_start--) { 483 | line = cm.getLineHandle(block_start); 484 | if(line.text.match(/^\s*$/)) { 485 | // empty or all whitespace - keep going 486 | continue; 487 | } else { 488 | if(code_type(cm, block_start, line) !== "indented") { 489 | block_start += 1; 490 | break; 491 | } 492 | } 493 | } 494 | lineCount = cm.lineCount(); 495 | for(block_end = cur_start.line; block_end < lineCount; block_end++) { 496 | line = cm.getLineHandle(block_end); 497 | if(line.text.match(/^\s*$/)) { 498 | // empty or all whitespace - keep going 499 | continue; 500 | } else { 501 | if(code_type(cm, block_end, line) !== "indented") { 502 | block_end -= 1; 503 | break; 504 | } 505 | } 506 | } 507 | } 508 | // if we are going to un-indent based on a selected set of lines, and the next line is indented too, we need to 509 | // insert a blank line so that the next line(s) continue to be indented code 510 | var next_line = cm.getLineHandle(block_end + 1), 511 | next_line_last_tok = next_line && cm.getTokenAt({ 512 | line: block_end + 1, 513 | ch: next_line.text.length - 1 514 | }), 515 | next_line_indented = next_line_last_tok && token_state(next_line_last_tok).indentedCode; 516 | if(next_line_indented) { 517 | cm.replaceRange("\n", { 518 | line: block_end + 1, 519 | ch: 0 520 | }); 521 | } 522 | 523 | for(var i = block_start; i <= block_end; i++) { 524 | cm.indentLine(i, "subtract"); // TODO: this doesn't get tracked in the history, so can't be undone :( 525 | } 526 | cm.focus(); 527 | } else { 528 | // insert code formatting 529 | var no_sel_and_starting_of_line = (cur_start.line === cur_end.line && cur_start.ch === cur_end.ch && cur_start.ch === 0); 530 | var sel_multi = cur_start.line !== cur_end.line; 531 | if(no_sel_and_starting_of_line || sel_multi) { 532 | insertFencingAtSelection(cm, cur_start, cur_end, fenceCharsToInsert); 533 | } else { 534 | _replaceSelection(cm, false, ["`", "`"]); 535 | } 536 | } 537 | } 538 | 539 | /** 540 | * Action for toggling blockquote. 541 | */ 542 | function toggleBlockquote(editor) { 543 | var cm = editor.codemirror; 544 | _toggleLine(cm, "quote"); 545 | } 546 | 547 | /** 548 | * Action for toggling heading size: normal -> h1 -> h2 -> h3 -> h4 -> h5 -> h6 -> normal 549 | */ 550 | function toggleHeadingSmaller(editor) { 551 | var cm = editor.codemirror; 552 | _toggleHeading(cm, "smaller"); 553 | } 554 | 555 | /** 556 | * Action for toggling heading size: normal -> h6 -> h5 -> h4 -> h3 -> h2 -> h1 -> normal 557 | */ 558 | function toggleHeadingBigger(editor) { 559 | var cm = editor.codemirror; 560 | _toggleHeading(cm, "bigger"); 561 | } 562 | 563 | /** 564 | * Action for toggling heading size 1 565 | */ 566 | function toggleHeading1(editor) { 567 | var cm = editor.codemirror; 568 | _toggleHeading(cm, undefined, 1); 569 | } 570 | 571 | /** 572 | * Action for toggling heading size 2 573 | */ 574 | function toggleHeading2(editor) { 575 | var cm = editor.codemirror; 576 | _toggleHeading(cm, undefined, 2); 577 | } 578 | 579 | /** 580 | * Action for toggling heading size 3 581 | */ 582 | function toggleHeading3(editor) { 583 | var cm = editor.codemirror; 584 | _toggleHeading(cm, undefined, 3); 585 | } 586 | 587 | 588 | /** 589 | * Action for toggling ul. 590 | */ 591 | function toggleUnorderedList(editor) { 592 | var cm = editor.codemirror; 593 | _toggleLine(cm, "unordered-list"); 594 | } 595 | 596 | 597 | /** 598 | * Action for toggling ol. 599 | */ 600 | function toggleOrderedList(editor) { 601 | var cm = editor.codemirror; 602 | _toggleLine(cm, "ordered-list"); 603 | } 604 | 605 | /** 606 | * Action for clean block (remove headline, list, blockquote code, markers) 607 | */ 608 | function cleanBlock(editor) { 609 | var cm = editor.codemirror; 610 | _cleanBlock(cm); 611 | } 612 | 613 | /** 614 | * Action for drawing a link. 615 | */ 616 | function drawLink(editor) { 617 | var cm = editor.codemirror; 618 | var stat = getState(cm); 619 | var options = editor.options; 620 | var url = "http://"; 621 | if(options.promptURLs) { 622 | url = prompt(options.promptTexts.link); 623 | if(!url) { 624 | return false; 625 | } 626 | } 627 | _replaceSelection(cm, stat.link, options.insertTexts.link, url); 628 | } 629 | 630 | /** 631 | * Action for drawing an img. 632 | */ 633 | function drawImage(editor) { 634 | var cm = editor.codemirror; 635 | var stat = getState(cm); 636 | var options = editor.options; 637 | var url = "http://"; 638 | if(options.promptURLs) { 639 | url = prompt(options.promptTexts.image); 640 | if(!url) { 641 | return false; 642 | } 643 | } 644 | _replaceSelection(cm, stat.image, options.insertTexts.image, url); 645 | } 646 | 647 | /** 648 | * Action for drawing a table. 649 | */ 650 | function drawTable(editor) { 651 | var cm = editor.codemirror; 652 | var stat = getState(cm); 653 | var options = editor.options; 654 | _replaceSelection(cm, stat.table, options.insertTexts.table); 655 | } 656 | 657 | /** 658 | * Action for drawing a horizontal rule. 659 | */ 660 | function drawHorizontalRule(editor) { 661 | var cm = editor.codemirror; 662 | var stat = getState(cm); 663 | var options = editor.options; 664 | _replaceSelection(cm, stat.image, options.insertTexts.horizontalRule); 665 | } 666 | 667 | 668 | /** 669 | * Undo action. 670 | */ 671 | function undo(editor) { 672 | var cm = editor.codemirror; 673 | cm.undo(); 674 | cm.focus(); 675 | } 676 | 677 | 678 | /** 679 | * Redo action. 680 | */ 681 | function redo(editor) { 682 | var cm = editor.codemirror; 683 | cm.redo(); 684 | cm.focus(); 685 | } 686 | 687 | 688 | /** 689 | * Toggle side by side preview 690 | */ 691 | function toggleSideBySide(editor) { 692 | var cm = editor.codemirror; 693 | var wrapper = cm.getWrapperElement(); 694 | var preview = wrapper.nextSibling; 695 | var toolbarButton = editor.toolbarElements["side-by-side"]; 696 | var useSideBySideListener = false; 697 | if(/editor-preview-active-side/.test(preview.className)) { 698 | preview.className = preview.className.replace( 699 | /\s*editor-preview-active-side\s*/g, "" 700 | ); 701 | toolbarButton.className = toolbarButton.className.replace(/\s*active\s*/g, ""); 702 | wrapper.className = wrapper.className.replace(/\s*CodeMirror-sided\s*/g, " "); 703 | } else { 704 | // When the preview button is clicked for the first time, 705 | // give some time for the transition from editor.css to fire and the view to slide from right to left, 706 | // instead of just appearing. 707 | setTimeout(function() { 708 | if(!cm.getOption("fullScreen")) 709 | toggleFullScreen(editor); 710 | preview.className += " editor-preview-active-side"; 711 | }, 1); 712 | toolbarButton.className += " active"; 713 | wrapper.className += " CodeMirror-sided"; 714 | useSideBySideListener = true; 715 | } 716 | 717 | // Hide normal preview if active 718 | var previewNormal = wrapper.lastChild; 719 | if(/editor-preview-active/.test(previewNormal.className)) { 720 | previewNormal.className = previewNormal.className.replace( 721 | /\s*editor-preview-active\s*/g, "" 722 | ); 723 | var toolbar = editor.toolbarElements.preview; 724 | var toolbar_div = wrapper.previousSibling; 725 | toolbar.className = toolbar.className.replace(/\s*active\s*/g, ""); 726 | toolbar_div.className = toolbar_div.className.replace(/\s*disabled-for-preview*/g, ""); 727 | } 728 | 729 | var sideBySideRenderingFunction = function() { 730 | preview.innerHTML = editor.options.previewRender(editor.value(), preview); 731 | }; 732 | 733 | if(!cm.sideBySideRenderingFunction) { 734 | cm.sideBySideRenderingFunction = sideBySideRenderingFunction; 735 | } 736 | 737 | if(useSideBySideListener) { 738 | preview.innerHTML = editor.options.previewRender(editor.value(), preview); 739 | cm.on("update", cm.sideBySideRenderingFunction); 740 | } else { 741 | cm.off("update", cm.sideBySideRenderingFunction); 742 | } 743 | 744 | // Refresh to fix selection being off (#309) 745 | cm.refresh(); 746 | } 747 | 748 | 749 | /** 750 | * Preview action. 751 | */ 752 | function togglePreview(editor) { 753 | var cm = editor.codemirror; 754 | var wrapper = cm.getWrapperElement(); 755 | var toolbar_div = wrapper.previousSibling; 756 | var toolbar = editor.options.toolbar ? editor.toolbarElements.preview : false; 757 | var preview = wrapper.lastChild; 758 | if(!preview || !/editor-preview/.test(preview.className)) { 759 | preview = document.createElement("div"); 760 | preview.className = "editor-preview"; 761 | wrapper.appendChild(preview); 762 | } 763 | if(/editor-preview-active/.test(preview.className)) { 764 | preview.className = preview.className.replace( 765 | /\s*editor-preview-active\s*/g, "" 766 | ); 767 | if(toolbar) { 768 | toolbar.className = toolbar.className.replace(/\s*active\s*/g, ""); 769 | toolbar_div.className = toolbar_div.className.replace(/\s*disabled-for-preview*/g, ""); 770 | } 771 | } else { 772 | // When the preview button is clicked for the first time, 773 | // give some time for the transition from editor.css to fire and the view to slide from right to left, 774 | // instead of just appearing. 775 | setTimeout(function() { 776 | preview.className += " editor-preview-active"; 777 | }, 1); 778 | if(toolbar) { 779 | toolbar.className += " active"; 780 | toolbar_div.className += " disabled-for-preview"; 781 | } 782 | } 783 | preview.innerHTML = editor.options.previewRender(editor.value(), preview); 784 | 785 | // Turn off side by side if needed 786 | var sidebyside = cm.getWrapperElement().nextSibling; 787 | if(/editor-preview-active-side/.test(sidebyside.className)) 788 | toggleSideBySide(editor); 789 | } 790 | 791 | function _replaceSelection(cm, active, startEnd, url) { 792 | if(/editor-preview-active/.test(cm.getWrapperElement().lastChild.className)) 793 | return; 794 | 795 | var text; 796 | var start = startEnd[0]; 797 | var end = startEnd[1]; 798 | var startPoint = cm.getCursor("start"); 799 | var endPoint = cm.getCursor("end"); 800 | if(url) { 801 | end = end.replace("#url#", url); 802 | } 803 | if(active) { 804 | text = cm.getLine(startPoint.line); 805 | start = text.slice(0, startPoint.ch); 806 | end = text.slice(startPoint.ch); 807 | cm.replaceRange(start + end, { 808 | line: startPoint.line, 809 | ch: 0 810 | }); 811 | } else { 812 | text = cm.getSelection(); 813 | cm.replaceSelection(start + text + end); 814 | 815 | startPoint.ch += start.length; 816 | if(startPoint !== endPoint) { 817 | endPoint.ch += start.length; 818 | } 819 | } 820 | cm.setSelection(startPoint, endPoint); 821 | cm.focus(); 822 | } 823 | 824 | 825 | function _toggleHeading(cm, direction, size) { 826 | if(/editor-preview-active/.test(cm.getWrapperElement().lastChild.className)) 827 | return; 828 | 829 | var startPoint = cm.getCursor("start"); 830 | var endPoint = cm.getCursor("end"); 831 | for(var i = startPoint.line; i <= endPoint.line; i++) { 832 | (function(i) { 833 | var text = cm.getLine(i); 834 | var currHeadingLevel = text.search(/[^#]/); 835 | 836 | if(direction !== undefined) { 837 | if(currHeadingLevel <= 0) { 838 | if(direction == "bigger") { 839 | text = "###### " + text; 840 | } else { 841 | text = "# " + text; 842 | } 843 | } else if(currHeadingLevel == 6 && direction == "smaller") { 844 | text = text.substr(7); 845 | } else if(currHeadingLevel == 1 && direction == "bigger") { 846 | text = text.substr(2); 847 | } else { 848 | if(direction == "bigger") { 849 | text = text.substr(1); 850 | } else { 851 | text = "#" + text; 852 | } 853 | } 854 | } else { 855 | if(size == 1) { 856 | if(currHeadingLevel <= 0) { 857 | text = "# " + text; 858 | } else if(currHeadingLevel == size) { 859 | text = text.substr(currHeadingLevel + 1); 860 | } else { 861 | text = "# " + text.substr(currHeadingLevel + 1); 862 | } 863 | } else if(size == 2) { 864 | if(currHeadingLevel <= 0) { 865 | text = "## " + text; 866 | } else if(currHeadingLevel == size) { 867 | text = text.substr(currHeadingLevel + 1); 868 | } else { 869 | text = "## " + text.substr(currHeadingLevel + 1); 870 | } 871 | } else { 872 | if(currHeadingLevel <= 0) { 873 | text = "### " + text; 874 | } else if(currHeadingLevel == size) { 875 | text = text.substr(currHeadingLevel + 1); 876 | } else { 877 | text = "### " + text.substr(currHeadingLevel + 1); 878 | } 879 | } 880 | } 881 | 882 | cm.replaceRange(text, { 883 | line: i, 884 | ch: 0 885 | }, { 886 | line: i, 887 | ch: 99999999999999 888 | }); 889 | })(i); 890 | } 891 | cm.focus(); 892 | } 893 | 894 | 895 | function _toggleLine(cm, name) { 896 | if(/editor-preview-active/.test(cm.getWrapperElement().lastChild.className)) 897 | return; 898 | 899 | var stat = getState(cm); 900 | var startPoint = cm.getCursor("start"); 901 | var endPoint = cm.getCursor("end"); 902 | var repl = { 903 | "quote": /^(\s*)\>\s+/, 904 | "unordered-list": /^(\s*)(\*|\-|\+)\s+/, 905 | "ordered-list": /^(\s*)\d+\.\s+/ 906 | }; 907 | var map = { 908 | "quote": "> ", 909 | "unordered-list": "* ", 910 | "ordered-list": "1. " 911 | }; 912 | for(var i = startPoint.line; i <= endPoint.line; i++) { 913 | (function(i) { 914 | var text = cm.getLine(i); 915 | if(stat[name]) { 916 | text = text.replace(repl[name], "$1"); 917 | } else { 918 | text = map[name] + text; 919 | } 920 | cm.replaceRange(text, { 921 | line: i, 922 | ch: 0 923 | }, { 924 | line: i, 925 | ch: 99999999999999 926 | }); 927 | })(i); 928 | } 929 | cm.focus(); 930 | } 931 | 932 | function _toggleBlock(editor, type, start_chars, end_chars) { 933 | if(/editor-preview-active/.test(editor.codemirror.getWrapperElement().lastChild.className)) 934 | return; 935 | 936 | end_chars = (typeof end_chars === "undefined") ? start_chars : end_chars; 937 | var cm = editor.codemirror; 938 | var stat = getState(cm); 939 | 940 | var text; 941 | var start = start_chars; 942 | var end = end_chars; 943 | 944 | var startPoint = cm.getCursor("start"); 945 | var endPoint = cm.getCursor("end"); 946 | 947 | if(stat[type]) { 948 | text = cm.getLine(startPoint.line); 949 | start = text.slice(0, startPoint.ch); 950 | end = text.slice(startPoint.ch); 951 | if(type == "bold") { 952 | start = start.replace(/(\*\*|__)(?![\s\S]*(\*\*|__))/, ""); 953 | end = end.replace(/(\*\*|__)/, ""); 954 | } else if(type == "italic") { 955 | start = start.replace(/(\*|_)(?![\s\S]*(\*|_))/, ""); 956 | end = end.replace(/(\*|_)/, ""); 957 | } else if(type == "strikethrough") { 958 | start = start.replace(/(\*\*|~~)(?![\s\S]*(\*\*|~~))/, ""); 959 | end = end.replace(/(\*\*|~~)/, ""); 960 | } 961 | cm.replaceRange(start + end, { 962 | line: startPoint.line, 963 | ch: 0 964 | }, { 965 | line: startPoint.line, 966 | ch: 99999999999999 967 | }); 968 | 969 | if(type == "bold" || type == "strikethrough") { 970 | startPoint.ch -= 2; 971 | if(startPoint !== endPoint) { 972 | endPoint.ch -= 2; 973 | } 974 | } else if(type == "italic") { 975 | startPoint.ch -= 1; 976 | if(startPoint !== endPoint) { 977 | endPoint.ch -= 1; 978 | } 979 | } 980 | } else { 981 | text = cm.getSelection(); 982 | if(type == "bold") { 983 | text = text.split("**").join(""); 984 | text = text.split("__").join(""); 985 | } else if(type == "italic") { 986 | text = text.split("*").join(""); 987 | text = text.split("_").join(""); 988 | } else if(type == "strikethrough") { 989 | text = text.split("~~").join(""); 990 | } 991 | cm.replaceSelection(start + text + end); 992 | 993 | startPoint.ch += start_chars.length; 994 | endPoint.ch = startPoint.ch + text.length; 995 | } 996 | 997 | cm.setSelection(startPoint, endPoint); 998 | cm.focus(); 999 | } 1000 | 1001 | function _cleanBlock(cm) { 1002 | if(/editor-preview-active/.test(cm.getWrapperElement().lastChild.className)) 1003 | return; 1004 | 1005 | var startPoint = cm.getCursor("start"); 1006 | var endPoint = cm.getCursor("end"); 1007 | var text; 1008 | 1009 | for(var line = startPoint.line; line <= endPoint.line; line++) { 1010 | text = cm.getLine(line); 1011 | text = text.replace(/^[ ]*([# ]+|\*|\-|[> ]+|[0-9]+(.|\)))[ ]*/, ""); 1012 | 1013 | cm.replaceRange(text, { 1014 | line: line, 1015 | ch: 0 1016 | }, { 1017 | line: line, 1018 | ch: 99999999999999 1019 | }); 1020 | } 1021 | } 1022 | 1023 | // Merge the properties of one object into another. 1024 | function _mergeProperties(target, source) { 1025 | for(var property in source) { 1026 | if(source.hasOwnProperty(property)) { 1027 | if(source[property] instanceof Array) { 1028 | target[property] = source[property].concat(target[property] instanceof Array ? target[property] : []); 1029 | } else if( 1030 | source[property] !== null && 1031 | typeof source[property] === "object" && 1032 | source[property].constructor === Object 1033 | ) { 1034 | target[property] = _mergeProperties(target[property] || {}, source[property]); 1035 | } else { 1036 | target[property] = source[property]; 1037 | } 1038 | } 1039 | } 1040 | 1041 | return target; 1042 | } 1043 | 1044 | // Merge an arbitrary number of objects into one. 1045 | function extend(target) { 1046 | for(var i = 1; i < arguments.length; i++) { 1047 | target = _mergeProperties(target, arguments[i]); 1048 | } 1049 | 1050 | return target; 1051 | } 1052 | 1053 | /* The right word count in respect for CJK. */ 1054 | function wordCount(data) { 1055 | var pattern = /[a-zA-Z0-9_\u0392-\u03c9\u0410-\u04F9]+|[\u4E00-\u9FFF\u3400-\u4dbf\uf900-\ufaff\u3040-\u309f\uac00-\ud7af]+/g; 1056 | var m = data.match(pattern); 1057 | var count = 0; 1058 | if(m === null) return count; 1059 | for(var i = 0; i < m.length; i++) { 1060 | if(m[i].charCodeAt(0) >= 0x4E00) { 1061 | count += m[i].length; 1062 | } else { 1063 | count += 1; 1064 | } 1065 | } 1066 | return count; 1067 | } 1068 | 1069 | var toolbarBuiltInButtons = { 1070 | "bold": { 1071 | name: "bold", 1072 | action: toggleBold, 1073 | className: "fa fa-bold", 1074 | title: "Bold", 1075 | default: true 1076 | }, 1077 | "italic": { 1078 | name: "italic", 1079 | action: toggleItalic, 1080 | className: "fa fa-italic", 1081 | title: "Italic", 1082 | default: true 1083 | }, 1084 | "strikethrough": { 1085 | name: "strikethrough", 1086 | action: toggleStrikethrough, 1087 | className: "fa fa-strikethrough", 1088 | title: "Strikethrough" 1089 | }, 1090 | "heading": { 1091 | name: "heading", 1092 | action: toggleHeadingSmaller, 1093 | className: "fa fa-header", 1094 | title: "Heading", 1095 | default: true 1096 | }, 1097 | "heading-smaller": { 1098 | name: "heading-smaller", 1099 | action: toggleHeadingSmaller, 1100 | className: "fa fa-header fa-header-x fa-header-smaller", 1101 | title: "Smaller Heading" 1102 | }, 1103 | "heading-bigger": { 1104 | name: "heading-bigger", 1105 | action: toggleHeadingBigger, 1106 | className: "fa fa-header fa-header-x fa-header-bigger", 1107 | title: "Bigger Heading" 1108 | }, 1109 | "heading-1": { 1110 | name: "heading-1", 1111 | action: toggleHeading1, 1112 | className: "fa fa-header fa-header-x fa-header-1", 1113 | title: "Big Heading" 1114 | }, 1115 | "heading-2": { 1116 | name: "heading-2", 1117 | action: toggleHeading2, 1118 | className: "fa fa-header fa-header-x fa-header-2", 1119 | title: "Medium Heading" 1120 | }, 1121 | "heading-3": { 1122 | name: "heading-3", 1123 | action: toggleHeading3, 1124 | className: "fa fa-header fa-header-x fa-header-3", 1125 | title: "Small Heading" 1126 | }, 1127 | "separator-1": { 1128 | name: "separator-1" 1129 | }, 1130 | "code": { 1131 | name: "code", 1132 | action: toggleCodeBlock, 1133 | className: "fa fa-code", 1134 | title: "Code" 1135 | }, 1136 | "quote": { 1137 | name: "quote", 1138 | action: toggleBlockquote, 1139 | className: "fa fa-quote-left", 1140 | title: "Quote", 1141 | default: true 1142 | }, 1143 | "unordered-list": { 1144 | name: "unordered-list", 1145 | action: toggleUnorderedList, 1146 | className: "fa fa-list-ul", 1147 | title: "Generic List", 1148 | default: true 1149 | }, 1150 | "ordered-list": { 1151 | name: "ordered-list", 1152 | action: toggleOrderedList, 1153 | className: "fa fa-list-ol", 1154 | title: "Numbered List", 1155 | default: true 1156 | }, 1157 | "clean-block": { 1158 | name: "clean-block", 1159 | action: cleanBlock, 1160 | className: "fa fa-eraser fa-clean-block", 1161 | title: "Clean block" 1162 | }, 1163 | "separator-2": { 1164 | name: "separator-2" 1165 | }, 1166 | "link": { 1167 | name: "link", 1168 | action: drawLink, 1169 | className: "fa fa-link", 1170 | title: "Create Link", 1171 | default: true 1172 | }, 1173 | "image": { 1174 | name: "image", 1175 | action: drawImage, 1176 | className: "fa fa-picture-o", 1177 | title: "Insert Image", 1178 | default: true 1179 | }, 1180 | "table": { 1181 | name: "table", 1182 | action: drawTable, 1183 | className: "fa fa-table", 1184 | title: "Insert Table" 1185 | }, 1186 | "horizontal-rule": { 1187 | name: "horizontal-rule", 1188 | action: drawHorizontalRule, 1189 | className: "fa fa-minus", 1190 | title: "Insert Horizontal Line" 1191 | }, 1192 | "separator-3": { 1193 | name: "separator-3" 1194 | }, 1195 | "preview": { 1196 | name: "preview", 1197 | action: togglePreview, 1198 | className: "fa fa-eye no-disable", 1199 | title: "Toggle Preview", 1200 | default: true 1201 | }, 1202 | "side-by-side": { 1203 | name: "side-by-side", 1204 | action: toggleSideBySide, 1205 | className: "fa fa-columns no-disable no-mobile", 1206 | title: "Toggle Side by Side", 1207 | default: true 1208 | }, 1209 | "fullscreen": { 1210 | name: "fullscreen", 1211 | action: toggleFullScreen, 1212 | className: "fa fa-arrows-alt no-disable no-mobile", 1213 | title: "Toggle Fullscreen", 1214 | default: true 1215 | }, 1216 | "separator-4": { 1217 | name: "separator-4" 1218 | }, 1219 | "guide": { 1220 | name: "guide", 1221 | action: "https://simplemde.com/markdown-guide", 1222 | className: "fa fa-question-circle", 1223 | title: "Markdown Guide", 1224 | default: true 1225 | }, 1226 | "separator-5": { 1227 | name: "separator-5" 1228 | }, 1229 | "undo": { 1230 | name: "undo", 1231 | action: undo, 1232 | className: "fa fa-undo no-disable", 1233 | title: "Undo" 1234 | }, 1235 | "redo": { 1236 | name: "redo", 1237 | action: redo, 1238 | className: "fa fa-repeat no-disable", 1239 | title: "Redo" 1240 | } 1241 | }; 1242 | 1243 | var insertTexts = { 1244 | link: ["[", "](#url#)"], 1245 | image: ["![](", "#url#)"], 1246 | table: ["", "\n\n| Column 1 | Column 2 | Column 3 |\n| -------- | -------- | -------- |\n| Text | Text | Text |\n\n"], 1247 | horizontalRule: ["", "\n\n-----\n\n"] 1248 | }; 1249 | 1250 | var promptTexts = { 1251 | link: "URL for the link:", 1252 | image: "URL of the image:" 1253 | }; 1254 | 1255 | var blockStyles = { 1256 | "bold": "**", 1257 | "code": "```", 1258 | "italic": "*" 1259 | }; 1260 | 1261 | /** 1262 | * Interface of SimpleMDE. 1263 | */ 1264 | function SimpleMDE(options) { 1265 | // Handle options parameter 1266 | options = options || {}; 1267 | 1268 | 1269 | // Used later to refer to it"s parent 1270 | options.parent = this; 1271 | 1272 | 1273 | // Check if Font Awesome needs to be auto downloaded 1274 | var autoDownloadFA = true; 1275 | 1276 | if(options.autoDownloadFontAwesome === false) { 1277 | autoDownloadFA = false; 1278 | } 1279 | 1280 | if(options.autoDownloadFontAwesome !== true) { 1281 | var styleSheets = document.styleSheets; 1282 | for(var i = 0; i < styleSheets.length; i++) { 1283 | if(!styleSheets[i].href) 1284 | continue; 1285 | 1286 | if(styleSheets[i].href.indexOf("//maxcdn.bootstrapcdn.com/font-awesome/") > -1) { 1287 | autoDownloadFA = false; 1288 | } 1289 | } 1290 | } 1291 | 1292 | if(autoDownloadFA) { 1293 | var link = document.createElement("link"); 1294 | link.rel = "stylesheet"; 1295 | link.href = "https://maxcdn.bootstrapcdn.com/font-awesome/latest/css/font-awesome.min.css"; 1296 | document.getElementsByTagName("head")[0].appendChild(link); 1297 | } 1298 | 1299 | 1300 | // Find the textarea to use 1301 | if(options.element) { 1302 | this.element = options.element; 1303 | } else if(options.element === null) { 1304 | // This means that the element option was specified, but no element was found 1305 | console.log("SimpleMDE: Error. No element was found."); 1306 | return; 1307 | } 1308 | 1309 | 1310 | // Handle toolbar 1311 | if(options.toolbar === undefined) { 1312 | // Initialize 1313 | options.toolbar = []; 1314 | 1315 | 1316 | // Loop over the built in buttons, to get the preferred order 1317 | for(var key in toolbarBuiltInButtons) { 1318 | if(toolbarBuiltInButtons.hasOwnProperty(key)) { 1319 | if(key.indexOf("separator-") != -1) { 1320 | options.toolbar.push("|"); 1321 | } 1322 | 1323 | if(toolbarBuiltInButtons[key].default === true || (options.showIcons && options.showIcons.constructor === Array && options.showIcons.indexOf(key) != -1)) { 1324 | options.toolbar.push(key); 1325 | } 1326 | } 1327 | } 1328 | } 1329 | 1330 | 1331 | // Handle status bar 1332 | if(!options.hasOwnProperty("status")) { 1333 | options.status = ["autosave", "lines", "words", "cursor"]; 1334 | } 1335 | 1336 | 1337 | // Add default preview rendering function 1338 | if(!options.previewRender) { 1339 | options.previewRender = function(plainText) { 1340 | // Note: "this" refers to the options object 1341 | return this.parent.markdown(plainText); 1342 | }; 1343 | } 1344 | 1345 | 1346 | // Set default options for parsing config 1347 | options.parsingConfig = extend({ 1348 | highlightFormatting: true // needed for toggleCodeBlock to detect types of code 1349 | }, options.parsingConfig || {}); 1350 | 1351 | 1352 | // Merging the insertTexts, with the given options 1353 | options.insertTexts = extend({}, insertTexts, options.insertTexts || {}); 1354 | 1355 | 1356 | // Merging the promptTexts, with the given options 1357 | options.promptTexts = promptTexts; 1358 | 1359 | 1360 | // Merging the blockStyles, with the given options 1361 | options.blockStyles = extend({}, blockStyles, options.blockStyles || {}); 1362 | 1363 | 1364 | // Merging the shortcuts, with the given options 1365 | options.shortcuts = extend({}, shortcuts, options.shortcuts || {}); 1366 | 1367 | 1368 | // Change unique_id to uniqueId for backwards compatibility 1369 | if(options.autosave != undefined && options.autosave.unique_id != undefined && options.autosave.unique_id != "") 1370 | options.autosave.uniqueId = options.autosave.unique_id; 1371 | 1372 | 1373 | // Update this options 1374 | this.options = options; 1375 | 1376 | 1377 | // Auto render 1378 | this.render(); 1379 | 1380 | 1381 | // The codemirror component is only available after rendering 1382 | // so, the setter for the initialValue can only run after 1383 | // the element has been rendered 1384 | if(options.initialValue && (!this.options.autosave || this.options.autosave.foundSavedValue !== true)) { 1385 | this.value(options.initialValue); 1386 | } 1387 | } 1388 | 1389 | /** 1390 | * Default markdown render. 1391 | */ 1392 | SimpleMDE.prototype.markdown = function(text) { 1393 | if(marked) { 1394 | // Initialize 1395 | var markedOptions = {}; 1396 | 1397 | 1398 | // Update options 1399 | if(this.options && this.options.renderingConfig && this.options.renderingConfig.singleLineBreaks === false) { 1400 | markedOptions.breaks = false; 1401 | } else { 1402 | markedOptions.breaks = true; 1403 | } 1404 | 1405 | if(this.options && this.options.renderingConfig && this.options.renderingConfig.codeSyntaxHighlighting === true && window.hljs) { 1406 | markedOptions.highlight = function(code) { 1407 | return window.hljs.highlightAuto(code).value; 1408 | }; 1409 | } 1410 | 1411 | 1412 | // Set options 1413 | marked.setOptions(markedOptions); 1414 | 1415 | 1416 | // Return 1417 | return marked(text); 1418 | } 1419 | }; 1420 | 1421 | /** 1422 | * Render editor to the given element. 1423 | */ 1424 | SimpleMDE.prototype.render = function(el) { 1425 | if(!el) { 1426 | el = this.element || document.getElementsByTagName("textarea")[0]; 1427 | } 1428 | 1429 | if(this._rendered && this._rendered === el) { 1430 | // Already rendered. 1431 | return; 1432 | } 1433 | 1434 | this.element = el; 1435 | var options = this.options; 1436 | 1437 | var self = this; 1438 | var keyMaps = {}; 1439 | 1440 | for(var key in options.shortcuts) { 1441 | // null stands for "do not bind this command" 1442 | if(options.shortcuts[key] !== null && bindings[key] !== null) { 1443 | (function(key) { 1444 | keyMaps[fixShortcut(options.shortcuts[key])] = function() { 1445 | bindings[key](self); 1446 | }; 1447 | })(key); 1448 | } 1449 | } 1450 | 1451 | keyMaps["Enter"] = "newlineAndIndentContinueMarkdownList"; 1452 | keyMaps["Tab"] = "tabAndIndentMarkdownList"; 1453 | keyMaps["Shift-Tab"] = "shiftTabAndUnindentMarkdownList"; 1454 | keyMaps["Esc"] = function(cm) { 1455 | if(cm.getOption("fullScreen")) toggleFullScreen(self); 1456 | }; 1457 | 1458 | document.addEventListener("keydown", function(e) { 1459 | e = e || window.event; 1460 | 1461 | if(e.keyCode == 27) { 1462 | if(self.codemirror.getOption("fullScreen")) toggleFullScreen(self); 1463 | } 1464 | }, false); 1465 | 1466 | var mode, backdrop; 1467 | if(options.spellChecker !== false) { 1468 | mode = "spell-checker"; 1469 | backdrop = options.parsingConfig; 1470 | backdrop.name = "gfm"; 1471 | backdrop.gitHubSpice = false; 1472 | 1473 | CodeMirrorSpellChecker({ 1474 | codeMirrorInstance: CodeMirror 1475 | }); 1476 | } else { 1477 | mode = options.parsingConfig; 1478 | mode.name = "gfm"; 1479 | mode.gitHubSpice = false; 1480 | } 1481 | 1482 | this.codemirror = CodeMirror.fromTextArea(el, { 1483 | mode: mode, 1484 | backdrop: backdrop, 1485 | theme: "paper", 1486 | tabSize: (options.tabSize != undefined) ? options.tabSize : 2, 1487 | indentUnit: (options.tabSize != undefined) ? options.tabSize : 2, 1488 | indentWithTabs: (options.indentWithTabs === false) ? false : true, 1489 | lineNumbers: false, 1490 | autofocus: (options.autofocus === true) ? true : false, 1491 | extraKeys: keyMaps, 1492 | lineWrapping: (options.lineWrapping === false) ? false : true, 1493 | allowDropFileTypes: ["text/plain"], 1494 | placeholder: options.placeholder || el.getAttribute("placeholder") || "", 1495 | styleSelectedText: (options.styleSelectedText != undefined) ? options.styleSelectedText : true 1496 | }); 1497 | 1498 | if(options.forceSync === true) { 1499 | var cm = this.codemirror; 1500 | cm.on("change", function() { 1501 | cm.save(); 1502 | }); 1503 | } 1504 | 1505 | this.gui = {}; 1506 | 1507 | if(options.toolbar !== false) { 1508 | this.gui.toolbar = this.createToolbar(); 1509 | } 1510 | if(options.status !== false) { 1511 | this.gui.statusbar = this.createStatusbar(); 1512 | } 1513 | if(options.autosave != undefined && options.autosave.enabled === true) { 1514 | this.autosave(); 1515 | } 1516 | 1517 | this.gui.sideBySide = this.createSideBySide(); 1518 | 1519 | this._rendered = this.element; 1520 | 1521 | 1522 | // Fixes CodeMirror bug (#344) 1523 | var temp_cm = this.codemirror; 1524 | setTimeout(function() { 1525 | temp_cm.refresh(); 1526 | }.bind(temp_cm), 0); 1527 | }; 1528 | 1529 | // Safari, in Private Browsing Mode, looks like it supports localStorage but all calls to setItem throw QuotaExceededError. We're going to detect this and set a variable accordingly. 1530 | function isLocalStorageAvailable() { 1531 | if(typeof localStorage === "object") { 1532 | try { 1533 | localStorage.setItem("smde_localStorage", 1); 1534 | localStorage.removeItem("smde_localStorage"); 1535 | } catch(e) { 1536 | return false; 1537 | } 1538 | } else { 1539 | return false; 1540 | } 1541 | 1542 | return true; 1543 | } 1544 | 1545 | SimpleMDE.prototype.autosave = function() { 1546 | if(isLocalStorageAvailable()) { 1547 | var simplemde = this; 1548 | 1549 | if(this.options.autosave.uniqueId == undefined || this.options.autosave.uniqueId == "") { 1550 | console.log("SimpleMDE: You must set a uniqueId to use the autosave feature"); 1551 | return; 1552 | } 1553 | 1554 | if(simplemde.element.form != null && simplemde.element.form != undefined) { 1555 | simplemde.element.form.addEventListener("submit", function() { 1556 | localStorage.removeItem("smde_" + simplemde.options.autosave.uniqueId); 1557 | }); 1558 | } 1559 | 1560 | if(this.options.autosave.loaded !== true) { 1561 | if(typeof localStorage.getItem("smde_" + this.options.autosave.uniqueId) == "string" && localStorage.getItem("smde_" + this.options.autosave.uniqueId) != "") { 1562 | this.codemirror.setValue(localStorage.getItem("smde_" + this.options.autosave.uniqueId)); 1563 | this.options.autosave.foundSavedValue = true; 1564 | } 1565 | 1566 | this.options.autosave.loaded = true; 1567 | } 1568 | 1569 | localStorage.setItem("smde_" + this.options.autosave.uniqueId, simplemde.value()); 1570 | 1571 | var el = document.getElementById("autosaved"); 1572 | if(el != null && el != undefined && el != "") { 1573 | var d = new Date(); 1574 | var hh = d.getHours(); 1575 | var m = d.getMinutes(); 1576 | var dd = "am"; 1577 | var h = hh; 1578 | if(h >= 12) { 1579 | h = hh - 12; 1580 | dd = "pm"; 1581 | } 1582 | if(h == 0) { 1583 | h = 12; 1584 | } 1585 | m = m < 10 ? "0" + m : m; 1586 | 1587 | el.innerHTML = "Autosaved: " + h + ":" + m + " " + dd; 1588 | } 1589 | 1590 | this.autosaveTimeoutId = setTimeout(function() { 1591 | simplemde.autosave(); 1592 | }, this.options.autosave.delay || 10000); 1593 | } else { 1594 | console.log("SimpleMDE: localStorage not available, cannot autosave"); 1595 | } 1596 | }; 1597 | 1598 | SimpleMDE.prototype.clearAutosavedValue = function() { 1599 | if(isLocalStorageAvailable()) { 1600 | if(this.options.autosave == undefined || this.options.autosave.uniqueId == undefined || this.options.autosave.uniqueId == "") { 1601 | console.log("SimpleMDE: You must set a uniqueId to clear the autosave value"); 1602 | return; 1603 | } 1604 | 1605 | localStorage.removeItem("smde_" + this.options.autosave.uniqueId); 1606 | } else { 1607 | console.log("SimpleMDE: localStorage not available, cannot autosave"); 1608 | } 1609 | }; 1610 | 1611 | SimpleMDE.prototype.createSideBySide = function() { 1612 | var cm = this.codemirror; 1613 | var wrapper = cm.getWrapperElement(); 1614 | var preview = wrapper.nextSibling; 1615 | 1616 | if(!preview || !/editor-preview-side/.test(preview.className)) { 1617 | preview = document.createElement("div"); 1618 | preview.className = "editor-preview-side"; 1619 | wrapper.parentNode.insertBefore(preview, wrapper.nextSibling); 1620 | } 1621 | 1622 | // Syncs scroll editor -> preview 1623 | var cScroll = false; 1624 | var pScroll = false; 1625 | cm.on("scroll", function(v) { 1626 | if(cScroll) { 1627 | cScroll = false; 1628 | return; 1629 | } 1630 | pScroll = true; 1631 | var height = v.getScrollInfo().height - v.getScrollInfo().clientHeight; 1632 | var ratio = parseFloat(v.getScrollInfo().top) / height; 1633 | var move = (preview.scrollHeight - preview.clientHeight) * ratio; 1634 | preview.scrollTop = move; 1635 | }); 1636 | 1637 | // Syncs scroll preview -> editor 1638 | preview.onscroll = function() { 1639 | if(pScroll) { 1640 | pScroll = false; 1641 | return; 1642 | } 1643 | cScroll = true; 1644 | var height = preview.scrollHeight - preview.clientHeight; 1645 | var ratio = parseFloat(preview.scrollTop) / height; 1646 | var move = (cm.getScrollInfo().height - cm.getScrollInfo().clientHeight) * ratio; 1647 | cm.scrollTo(0, move); 1648 | }; 1649 | return preview; 1650 | }; 1651 | 1652 | SimpleMDE.prototype.createToolbar = function(items) { 1653 | items = items || this.options.toolbar; 1654 | 1655 | if(!items || items.length === 0) { 1656 | return; 1657 | } 1658 | var i; 1659 | for(i = 0; i < items.length; i++) { 1660 | if(toolbarBuiltInButtons[items[i]] != undefined) { 1661 | items[i] = toolbarBuiltInButtons[items[i]]; 1662 | } 1663 | } 1664 | 1665 | var bar = document.createElement("div"); 1666 | bar.className = "editor-toolbar"; 1667 | 1668 | var self = this; 1669 | 1670 | var toolbarData = {}; 1671 | self.toolbar = items; 1672 | 1673 | for(i = 0; i < items.length; i++) { 1674 | if(items[i].name == "guide" && self.options.toolbarGuideIcon === false) 1675 | continue; 1676 | 1677 | if(self.options.hideIcons && self.options.hideIcons.indexOf(items[i].name) != -1) 1678 | continue; 1679 | 1680 | // Fullscreen does not work well on mobile devices (even tablets) 1681 | // In the future, hopefully this can be resolved 1682 | if((items[i].name == "fullscreen" || items[i].name == "side-by-side") && isMobile()) 1683 | continue; 1684 | 1685 | 1686 | // Don't include trailing separators 1687 | if(items[i] === "|") { 1688 | var nonSeparatorIconsFollow = false; 1689 | 1690 | for(var x = (i + 1); x < items.length; x++) { 1691 | if(items[x] !== "|" && (!self.options.hideIcons || self.options.hideIcons.indexOf(items[x].name) == -1)) { 1692 | nonSeparatorIconsFollow = true; 1693 | } 1694 | } 1695 | 1696 | if(!nonSeparatorIconsFollow) 1697 | continue; 1698 | } 1699 | 1700 | 1701 | // Create the icon and append to the toolbar 1702 | (function(item) { 1703 | var el; 1704 | if(item === "|") { 1705 | el = createSep(); 1706 | } else { 1707 | el = createIcon(item, self.options.toolbarTips, self.options.shortcuts); 1708 | } 1709 | 1710 | // bind events, special for info 1711 | if(item.action) { 1712 | if(typeof item.action === "function") { 1713 | el.onclick = function(e) { 1714 | e.preventDefault(); 1715 | item.action(self); 1716 | }; 1717 | } else if(typeof item.action === "string") { 1718 | el.href = item.action; 1719 | el.target = "_blank"; 1720 | } 1721 | } 1722 | 1723 | toolbarData[item.name || item] = el; 1724 | bar.appendChild(el); 1725 | })(items[i]); 1726 | } 1727 | 1728 | self.toolbarElements = toolbarData; 1729 | 1730 | var cm = this.codemirror; 1731 | cm.on("cursorActivity", function() { 1732 | var stat = getState(cm); 1733 | 1734 | for(var key in toolbarData) { 1735 | (function(key) { 1736 | var el = toolbarData[key]; 1737 | if(stat[key]) { 1738 | el.className += " active"; 1739 | } else if(key != "fullscreen" && key != "side-by-side") { 1740 | el.className = el.className.replace(/\s*active\s*/g, ""); 1741 | } 1742 | })(key); 1743 | } 1744 | }); 1745 | 1746 | var cmWrapper = cm.getWrapperElement(); 1747 | cmWrapper.parentNode.insertBefore(bar, cmWrapper); 1748 | return bar; 1749 | }; 1750 | 1751 | SimpleMDE.prototype.createStatusbar = function(status) { 1752 | // Initialize 1753 | status = status || this.options.status; 1754 | var options = this.options; 1755 | var cm = this.codemirror; 1756 | 1757 | 1758 | // Make sure the status variable is valid 1759 | if(!status || status.length === 0) 1760 | return; 1761 | 1762 | 1763 | // Set up the built-in items 1764 | var items = []; 1765 | var i, onUpdate, defaultValue; 1766 | 1767 | for(i = 0; i < status.length; i++) { 1768 | // Reset some values 1769 | onUpdate = undefined; 1770 | defaultValue = undefined; 1771 | 1772 | 1773 | // Handle if custom or not 1774 | if(typeof status[i] === "object") { 1775 | items.push({ 1776 | className: status[i].className, 1777 | defaultValue: status[i].defaultValue, 1778 | onUpdate: status[i].onUpdate 1779 | }); 1780 | } else { 1781 | var name = status[i]; 1782 | 1783 | if(name === "words") { 1784 | defaultValue = function(el) { 1785 | el.innerHTML = wordCount(cm.getValue()); 1786 | }; 1787 | onUpdate = function(el) { 1788 | el.innerHTML = wordCount(cm.getValue()); 1789 | }; 1790 | } else if(name === "lines") { 1791 | defaultValue = function(el) { 1792 | el.innerHTML = cm.lineCount(); 1793 | }; 1794 | onUpdate = function(el) { 1795 | el.innerHTML = cm.lineCount(); 1796 | }; 1797 | } else if(name === "cursor") { 1798 | defaultValue = function(el) { 1799 | el.innerHTML = "0:0"; 1800 | }; 1801 | onUpdate = function(el) { 1802 | var pos = cm.getCursor(); 1803 | el.innerHTML = pos.line + ":" + pos.ch; 1804 | }; 1805 | } else if(name === "autosave") { 1806 | defaultValue = function(el) { 1807 | if(options.autosave != undefined && options.autosave.enabled === true) { 1808 | el.setAttribute("id", "autosaved"); 1809 | } 1810 | }; 1811 | } 1812 | 1813 | items.push({ 1814 | className: name, 1815 | defaultValue: defaultValue, 1816 | onUpdate: onUpdate 1817 | }); 1818 | } 1819 | } 1820 | 1821 | 1822 | // Create element for the status bar 1823 | var bar = document.createElement("div"); 1824 | bar.className = "editor-statusbar"; 1825 | 1826 | 1827 | // Create a new span for each item 1828 | for(i = 0; i < items.length; i++) { 1829 | // Store in temporary variable 1830 | var item = items[i]; 1831 | 1832 | 1833 | // Create span element 1834 | var el = document.createElement("span"); 1835 | el.className = item.className; 1836 | 1837 | 1838 | // Ensure the defaultValue is a function 1839 | if(typeof item.defaultValue === "function") { 1840 | item.defaultValue(el); 1841 | } 1842 | 1843 | 1844 | // Ensure the onUpdate is a function 1845 | if(typeof item.onUpdate === "function") { 1846 | // Create a closure around the span of the current action, then execute the onUpdate handler 1847 | this.codemirror.on("update", (function(el, item) { 1848 | return function() { 1849 | item.onUpdate(el); 1850 | }; 1851 | }(el, item))); 1852 | } 1853 | 1854 | 1855 | // Append the item to the status bar 1856 | bar.appendChild(el); 1857 | } 1858 | 1859 | 1860 | // Insert the status bar into the DOM 1861 | var cmWrapper = this.codemirror.getWrapperElement(); 1862 | cmWrapper.parentNode.insertBefore(bar, cmWrapper.nextSibling); 1863 | return bar; 1864 | }; 1865 | 1866 | /** 1867 | * Get or set the text content. 1868 | */ 1869 | SimpleMDE.prototype.value = function(val) { 1870 | if(val === undefined) { 1871 | return this.codemirror.getValue(); 1872 | } else { 1873 | this.codemirror.getDoc().setValue(val); 1874 | return this; 1875 | } 1876 | }; 1877 | 1878 | 1879 | /** 1880 | * Bind static methods for exports. 1881 | */ 1882 | SimpleMDE.toggleBold = toggleBold; 1883 | SimpleMDE.toggleItalic = toggleItalic; 1884 | SimpleMDE.toggleStrikethrough = toggleStrikethrough; 1885 | SimpleMDE.toggleBlockquote = toggleBlockquote; 1886 | SimpleMDE.toggleHeadingSmaller = toggleHeadingSmaller; 1887 | SimpleMDE.toggleHeadingBigger = toggleHeadingBigger; 1888 | SimpleMDE.toggleHeading1 = toggleHeading1; 1889 | SimpleMDE.toggleHeading2 = toggleHeading2; 1890 | SimpleMDE.toggleHeading3 = toggleHeading3; 1891 | SimpleMDE.toggleCodeBlock = toggleCodeBlock; 1892 | SimpleMDE.toggleUnorderedList = toggleUnorderedList; 1893 | SimpleMDE.toggleOrderedList = toggleOrderedList; 1894 | SimpleMDE.cleanBlock = cleanBlock; 1895 | SimpleMDE.drawLink = drawLink; 1896 | SimpleMDE.drawImage = drawImage; 1897 | SimpleMDE.drawTable = drawTable; 1898 | SimpleMDE.drawHorizontalRule = drawHorizontalRule; 1899 | SimpleMDE.undo = undo; 1900 | SimpleMDE.redo = redo; 1901 | SimpleMDE.togglePreview = togglePreview; 1902 | SimpleMDE.toggleSideBySide = toggleSideBySide; 1903 | SimpleMDE.toggleFullScreen = toggleFullScreen; 1904 | 1905 | /** 1906 | * Bind instance methods for exports. 1907 | */ 1908 | SimpleMDE.prototype.toggleBold = function() { 1909 | toggleBold(this); 1910 | }; 1911 | SimpleMDE.prototype.toggleItalic = function() { 1912 | toggleItalic(this); 1913 | }; 1914 | SimpleMDE.prototype.toggleStrikethrough = function() { 1915 | toggleStrikethrough(this); 1916 | }; 1917 | SimpleMDE.prototype.toggleBlockquote = function() { 1918 | toggleBlockquote(this); 1919 | }; 1920 | SimpleMDE.prototype.toggleHeadingSmaller = function() { 1921 | toggleHeadingSmaller(this); 1922 | }; 1923 | SimpleMDE.prototype.toggleHeadingBigger = function() { 1924 | toggleHeadingBigger(this); 1925 | }; 1926 | SimpleMDE.prototype.toggleHeading1 = function() { 1927 | toggleHeading1(this); 1928 | }; 1929 | SimpleMDE.prototype.toggleHeading2 = function() { 1930 | toggleHeading2(this); 1931 | }; 1932 | SimpleMDE.prototype.toggleHeading3 = function() { 1933 | toggleHeading3(this); 1934 | }; 1935 | SimpleMDE.prototype.toggleCodeBlock = function() { 1936 | toggleCodeBlock(this); 1937 | }; 1938 | SimpleMDE.prototype.toggleUnorderedList = function() { 1939 | toggleUnorderedList(this); 1940 | }; 1941 | SimpleMDE.prototype.toggleOrderedList = function() { 1942 | toggleOrderedList(this); 1943 | }; 1944 | SimpleMDE.prototype.cleanBlock = function() { 1945 | cleanBlock(this); 1946 | }; 1947 | SimpleMDE.prototype.drawLink = function() { 1948 | drawLink(this); 1949 | }; 1950 | SimpleMDE.prototype.drawImage = function() { 1951 | drawImage(this); 1952 | }; 1953 | SimpleMDE.prototype.drawTable = function() { 1954 | drawTable(this); 1955 | }; 1956 | SimpleMDE.prototype.drawHorizontalRule = function() { 1957 | drawHorizontalRule(this); 1958 | }; 1959 | SimpleMDE.prototype.undo = function() { 1960 | undo(this); 1961 | }; 1962 | SimpleMDE.prototype.redo = function() { 1963 | redo(this); 1964 | }; 1965 | SimpleMDE.prototype.togglePreview = function() { 1966 | togglePreview(this); 1967 | }; 1968 | SimpleMDE.prototype.toggleSideBySide = function() { 1969 | toggleSideBySide(this); 1970 | }; 1971 | SimpleMDE.prototype.toggleFullScreen = function() { 1972 | toggleFullScreen(this); 1973 | }; 1974 | 1975 | SimpleMDE.prototype.isPreviewActive = function() { 1976 | var cm = this.codemirror; 1977 | var wrapper = cm.getWrapperElement(); 1978 | var preview = wrapper.lastChild; 1979 | 1980 | return /editor-preview-active/.test(preview.className); 1981 | }; 1982 | 1983 | SimpleMDE.prototype.isSideBySideActive = function() { 1984 | var cm = this.codemirror; 1985 | var wrapper = cm.getWrapperElement(); 1986 | var preview = wrapper.nextSibling; 1987 | 1988 | return /editor-preview-active-side/.test(preview.className); 1989 | }; 1990 | 1991 | SimpleMDE.prototype.isFullscreenActive = function() { 1992 | var cm = this.codemirror; 1993 | 1994 | return cm.getOption("fullScreen"); 1995 | }; 1996 | 1997 | SimpleMDE.prototype.getState = function() { 1998 | var cm = this.codemirror; 1999 | 2000 | return getState(cm); 2001 | }; 2002 | 2003 | SimpleMDE.prototype.toTextArea = function() { 2004 | var cm = this.codemirror; 2005 | var wrapper = cm.getWrapperElement(); 2006 | 2007 | if(wrapper.parentNode) { 2008 | if(this.gui.toolbar) { 2009 | wrapper.parentNode.removeChild(this.gui.toolbar); 2010 | } 2011 | if(this.gui.statusbar) { 2012 | wrapper.parentNode.removeChild(this.gui.statusbar); 2013 | } 2014 | if(this.gui.sideBySide) { 2015 | wrapper.parentNode.removeChild(this.gui.sideBySide); 2016 | } 2017 | } 2018 | 2019 | cm.toTextArea(); 2020 | 2021 | if(this.autosaveTimeoutId) { 2022 | clearTimeout(this.autosaveTimeoutId); 2023 | this.autosaveTimeoutId = undefined; 2024 | this.clearAutosavedValue(); 2025 | } 2026 | }; 2027 | 2028 | module.exports = SimpleMDE; --------------------------------------------------------------------------------