MarkMark - add sha256 hashing (+update HTML renderer to insert IDs), update MD renderer to include dates, add syndication for bookmarks
This commit is contained in:
parent
832190f875
commit
b5450c6100
@ -2,12 +2,16 @@ import {appPath, Controller, Inject, Injectable, plaintext, view} from '@extollo
|
||||
import {Home} from './Home.controller'
|
||||
import * as marked from 'marked'
|
||||
import {MarkMarkService} from '../../services/MarkMark.service'
|
||||
import {MarkMarkBlog} from '../../services/blog/MarkMarkBlog.service'
|
||||
|
||||
@Injectable()
|
||||
export class MarkMark extends Controller {
|
||||
@Inject()
|
||||
protected readonly markmark!: MarkMarkService
|
||||
|
||||
@Inject()
|
||||
protected readonly blog!: MarkMarkBlog
|
||||
|
||||
public welcome() {
|
||||
const home = <Home> this.make(Home)
|
||||
return view('markmark:welcome', {
|
||||
@ -47,4 +51,18 @@ export class MarkMark extends Controller {
|
||||
.contentType('text/markdown;variant=markmark')
|
||||
}
|
||||
|
||||
public async rss() {
|
||||
const feed = await this.blog.getFeed()
|
||||
return plaintext(feed.rss2()).contentType('application/rss+xml; charset=UTF-8')
|
||||
}
|
||||
|
||||
public async atom() {
|
||||
const feed = await this.blog.getFeed()
|
||||
return plaintext(feed.atom1()).contentType('application/atom+xml; charset=UTF-8')
|
||||
}
|
||||
|
||||
public async json() {
|
||||
const feed = await this.blog.getFeed()
|
||||
return plaintext(feed.json1()).contentType('application/feed+json; charset=UTF-8')
|
||||
}
|
||||
}
|
||||
|
@ -184,6 +184,19 @@ Route
|
||||
Route.get('/links')
|
||||
.pre(SiteTheme)
|
||||
.calls<MarkMark>(MarkMark, m => m.linksHtml)
|
||||
.alias('links')
|
||||
|
||||
Route.get('/links/rss2.xml')
|
||||
.calls<MarkMark>(MarkMark, m => m.rss)
|
||||
.alias('links:rss')
|
||||
|
||||
Route.get('/links/atom.xml')
|
||||
.calls<MarkMark>(MarkMark, m => m.atom)
|
||||
.alias('links:atom')
|
||||
|
||||
Route.get('/links/json.json')
|
||||
.calls<MarkMark>(MarkMark, m => m.json)
|
||||
.alias('links:json')
|
||||
|
||||
Route.get('/links.mark.md')
|
||||
.calls<MarkMark>(MarkMark, m => m.linksMarkMark)
|
||||
|
@ -10,7 +10,12 @@ block content
|
||||
h1 Bookmarks
|
||||
p Below is a random collection of links to other sites that I found interesting.
|
||||
p I think publishing personal bookmark lists is a great way to better connect and explore the smaller side of the Internet.
|
||||
p This list is also available in <a href="#{route('/links.mark.md')}"><img class="inline-markmark-logo" src="#{asset(textIsLight ? 'markmark-light.svg' : 'markmark-dark.svg')}"/> MarkMark format</a> (<a href="#{route('/markmark')}">learn more</a>).
|
||||
p This list is also available in the following formats:
|
||||
ul
|
||||
li <a href="#{route('/links.mark.md')}"><img class="inline-markmark-logo" src="#{asset(textIsLight ? 'markmark-light.svg' : 'markmark-dark.svg')}"/> MarkMark</a> (<a href="#{route('/markmark')}">learn more</a>).
|
||||
li <a href="#{named('links:rss')}">RSS</a>
|
||||
li <a href="#{named('links:atom')}">Atom</a>
|
||||
li <a href="#{named('links:json')}">JSON</a>
|
||||
section#links !{contentHtml}
|
||||
|
||||
block append style
|
||||
|
@ -31,9 +31,15 @@ head
|
||||
link(rel='author' href='/humans.txt')
|
||||
link(rel="alternate" href="/links.mark.md" title="Garrett Mills - My Bookmarks" type="text/markdown;variant=markmark")
|
||||
link(rel="alternate" href="/links" title="Garrett Mills - My Bookmarks" type="text/html")
|
||||
link(rel="alternate" href="/feed/atom.xml" title="Garrett Mills - Posts & Updates" type="application/atom+xml")
|
||||
link(rel="alternate" href="/feed/rss.xml" title="Garrett Mills - Posts & Updates" type="application/rss+xml")
|
||||
link(rel="alternate" href="/feed/json.json" title="Garrett Mills - Posts & Updates" type="application/feed+json")
|
||||
link(rel="alternate" href="/links/atom.xml" title="Garrett's Bookmarks (Atom)" type="application/atom+xml")
|
||||
link(rel="alternate" href="/links/rss2.xml" title="Garrett's Bookmarks (RSS)" type="application/rss+xml")
|
||||
link(rel="alternate" href="/links/json.json" title="Garrett's Bookmarks (JSON)" type="application/feed+json")
|
||||
link(rel="alternate" href="/feed/atom.xml" title="Garrett Mills - Posts & Updates (Atom)" type="application/atom+xml")
|
||||
link(rel="alternate" href="/feed/rss.xml" title="Garrett Mills - Posts & Updates (RSS)" type="application/rss+xml")
|
||||
link(rel="alternate" href="/feed/json.json" title="Garrett Mills - Posts & Updates (JSON)" type="application/feed+json")
|
||||
link(rel="alternate" href="/blog/atom.xml" title="Garrett's Blog (Atom)" type="application/atom+xml")
|
||||
link(rel="alternate" href="/blog/rss2.xml" title="Garrett's Blog (RSS)" type="application/rss+xml")
|
||||
link(rel="alternate" href="/blog/json.json" title="Garrett's Blog (JSON)" type="application/feed+json")
|
||||
|
||||
link(rel='apple-touch-icon' sizes='180x180' href=asset('favicon/apple-touch-icon.png'))
|
||||
link(rel='manifest' href=asset('favicon/site.webmanifest'))
|
||||
|
84
src/app/services/blog/MarkMarkBlog.service.ts
Normal file
84
src/app/services/blog/MarkMarkBlog.service.ts
Normal file
@ -0,0 +1,84 @@
|
||||
import * as RSSFeed from 'feed'
|
||||
import {Awaitable, collect, Collection, Inject, Maybe, Singleton} from '@extollo/lib'
|
||||
import {AbstractBlog, BlogBackend, BlogPost, BlogPostFrontMatter, isBlogPostFrontMatter} from './AbstractBlog.service'
|
||||
import {MarkMarkService} from '../MarkMark.service'
|
||||
import {Link} from '../../../markmark/types'
|
||||
import {MarkMarkRenderer} from '../../../markmark/markmark.renderer'
|
||||
|
||||
@Singleton()
|
||||
export class MarkMarkBlog extends AbstractBlog<BlogPostFrontMatter> {
|
||||
@Inject()
|
||||
protected readonly mark!: MarkMarkService
|
||||
|
||||
protected getBackend(): BlogBackend<BlogPostFrontMatter> {
|
||||
return {
|
||||
routePrefix: '/links',
|
||||
resourcePath: [],
|
||||
author: {
|
||||
name: 'Garrett Mills',
|
||||
email: 'shout@garrettmills.dev',
|
||||
link: 'https://garrettmills.dev/#about',
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
protected isValidFrontMatter(what: unknown): what is BlogPostFrontMatter {
|
||||
return isBlogPostFrontMatter(what)
|
||||
}
|
||||
|
||||
async getAllPosts(): Promise<Collection<BlogPost>> {
|
||||
const mm = await this.mark.getLinks()
|
||||
|
||||
const links = mm.sections
|
||||
.map(s => s.links)
|
||||
.reduce((l, c): Link[] => [...c, ...l], [])
|
||||
|
||||
return collect(links)
|
||||
.filter(l => l.date)
|
||||
.sort((a, b) => b.date!.getTime() - a.date!.getTime())
|
||||
.map(l => ({
|
||||
title: l.title,
|
||||
slug: l.hash,
|
||||
date: l.date!,
|
||||
tags: l.tags,
|
||||
file: l.hash, // not used
|
||||
markdown: (new MarkMarkRenderer()).render({
|
||||
...mm,
|
||||
sections: [{
|
||||
links: [l],
|
||||
}],
|
||||
}, false),
|
||||
}))
|
||||
}
|
||||
|
||||
protected createFeed(lastUpdated: Maybe<Date>): Awaitable<RSSFeed.Feed> {
|
||||
return new RSSFeed.Feed({
|
||||
title: 'Garrett\'s Bookmarks',
|
||||
description: 'Links to articles, projects, and sites I found interesting',
|
||||
id: this.routing.getNamedPath('links').toRemote,
|
||||
link: this.routing.getNamedPath('links').toRemote,
|
||||
language: 'en',
|
||||
image: this.routing.getAssetPath('favicon', 'apple-touch-icon.png').toRemote,
|
||||
favicon: this.routing.getAssetPath('favicon', 'favicon.ico').toRemote,
|
||||
copyright: `Copyright (c) ${(new Date).getFullYear()} Garrett Mills. See website for licensing details.`,
|
||||
updated: lastUpdated,
|
||||
generator: '',
|
||||
feedLinks: {
|
||||
json: this.routing.getNamedPath('links:json').toRemote,
|
||||
atom: this.routing.getNamedPath('links:atom').toRemote,
|
||||
rss: this.routing.getNamedPath('links:rss').toRemote,
|
||||
},
|
||||
author: {
|
||||
name: 'Garrett Mills',
|
||||
email: 'shout@garrettmills.dev',
|
||||
link: 'https://garrettmills.dev/#about',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
getUrl(post: BlogPost): string {
|
||||
let prefix = this.getBackend().routePrefix
|
||||
if ( !prefix.startsWith('/') ) prefix = `/${prefix}`
|
||||
return `${prefix}/#link-${post.slug}`
|
||||
}
|
||||
}
|
@ -29,7 +29,7 @@ export class HtmlRenderer {
|
||||
linkTitle += ` <span class="markmark link-tags">${link.tags.map(x => '<span class="markmark link-tag">#' + x + '</span>').join(' ')}</span>`
|
||||
}
|
||||
|
||||
mmLines.push(`<li class="markmark link-title">${marked.marked.parse(linkTitle)}<ul class="markmark url-list">`)
|
||||
mmLines.push(`<li class="markmark link-title" id="link-${link.hash}">${marked.marked.parse(linkTitle)}<ul class="markmark url-list">`)
|
||||
|
||||
for ( const url of link.urls ) {
|
||||
mmLines.push(`<li class="markmark link-url"><a href="${url}" target="_blank">${url}</a></li>`)
|
||||
|
@ -1,14 +1,16 @@
|
||||
import {isNamedSection, MarkMark} from './types'
|
||||
|
||||
export class MarkMarkRenderer {
|
||||
public render(mm: MarkMark): string {
|
||||
public render(mm: MarkMark, frontmatter: boolean = true): string {
|
||||
let mmLines: string[] = ['\n']
|
||||
|
||||
// Write the frontmatter
|
||||
if ( frontmatter ) {
|
||||
mmLines.push(`[//]: #(markmark-syntax: ${mm.frontmatter.syntax})`)
|
||||
if ( mm.frontmatter.authorName ) mmLines.push(`[//]: #(markmark-author-name: ${mm.frontmatter.authorName})`)
|
||||
if ( mm.frontmatter.authorEmail ) mmLines.push(`[//]: #(markmark-author-email: ${mm.frontmatter.authorEmail})`)
|
||||
if ( mm.frontmatter.authorHref ) mmLines.push(`[//]: #(markmark-author-href: ${mm.frontmatter.authorHref})`)
|
||||
if (mm.frontmatter.authorName) mmLines.push(`[//]: #(markmark-author-name: ${mm.frontmatter.authorName})`)
|
||||
if (mm.frontmatter.authorEmail) mmLines.push(`[//]: #(markmark-author-email: ${mm.frontmatter.authorEmail})`)
|
||||
if (mm.frontmatter.authorHref) mmLines.push(`[//]: #(markmark-author-href: ${mm.frontmatter.authorHref})`)
|
||||
}
|
||||
|
||||
for ( const section of mm.sections ) {
|
||||
mmLines.push('\n')
|
||||
@ -23,6 +25,11 @@ export class MarkMarkRenderer {
|
||||
|
||||
for ( const link of section.links ) {
|
||||
let linkTitle = `- ${link.title}`
|
||||
|
||||
if ( link.date ) {
|
||||
linkTitle += ` (${this.formatDate(link.date)})`
|
||||
}
|
||||
|
||||
if ( link.tags.length ) {
|
||||
linkTitle += ` ${link.tags.map(x => '#' + x).join(' ')}`
|
||||
}
|
||||
@ -37,4 +44,11 @@ export class MarkMarkRenderer {
|
||||
|
||||
return mmLines.join('\n')
|
||||
}
|
||||
|
||||
private formatDate(date: Date): string {
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0'); // Months are zero-based
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
return `${year}-${month}-${day}`;
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
import * as marked from 'marked'
|
||||
import {FrontMatter, isNamedSection, Link, MarkMark, Section} from './types'
|
||||
import * as crypto from 'crypto'
|
||||
|
||||
export class Parser {
|
||||
public parse(content: string): MarkMark {
|
||||
@ -65,6 +66,7 @@ export class Parser {
|
||||
currentLink = {
|
||||
title,
|
||||
date,
|
||||
hash: crypto.createHash('sha256').update(token.text).digest('hex'),
|
||||
tags: this.parseTags(token.text),
|
||||
urls: [],
|
||||
}
|
||||
|
@ -20,6 +20,7 @@ export const isNamedSection = (what: Section): what is NamedSection =>
|
||||
hasOwnProperty(what, 'title') && (typeof what.title === 'string')
|
||||
|
||||
export type Link = {
|
||||
hash: string,
|
||||
title: string,
|
||||
date?: Date,
|
||||
tags: string[],
|
||||
|
Loading…
Reference in New Issue
Block a user