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 {Home} from './Home.controller'
|
||||||
import * as marked from 'marked'
|
import * as marked from 'marked'
|
||||||
import {MarkMarkService} from '../../services/MarkMark.service'
|
import {MarkMarkService} from '../../services/MarkMark.service'
|
||||||
|
import {MarkMarkBlog} from '../../services/blog/MarkMarkBlog.service'
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class MarkMark extends Controller {
|
export class MarkMark extends Controller {
|
||||||
@Inject()
|
@Inject()
|
||||||
protected readonly markmark!: MarkMarkService
|
protected readonly markmark!: MarkMarkService
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
protected readonly blog!: MarkMarkBlog
|
||||||
|
|
||||||
public welcome() {
|
public welcome() {
|
||||||
const home = <Home> this.make(Home)
|
const home = <Home> this.make(Home)
|
||||||
return view('markmark:welcome', {
|
return view('markmark:welcome', {
|
||||||
@ -47,4 +51,18 @@ export class MarkMark extends Controller {
|
|||||||
.contentType('text/markdown;variant=markmark')
|
.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')
|
Route.get('/links')
|
||||||
.pre(SiteTheme)
|
.pre(SiteTheme)
|
||||||
.calls<MarkMark>(MarkMark, m => m.linksHtml)
|
.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')
|
Route.get('/links.mark.md')
|
||||||
.calls<MarkMark>(MarkMark, m => m.linksMarkMark)
|
.calls<MarkMark>(MarkMark, m => m.linksMarkMark)
|
||||||
|
@ -10,7 +10,12 @@ block content
|
|||||||
h1 Bookmarks
|
h1 Bookmarks
|
||||||
p Below is a random collection of links to other sites that I found interesting.
|
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 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}
|
section#links !{contentHtml}
|
||||||
|
|
||||||
block append style
|
block append style
|
||||||
|
@ -31,9 +31,15 @@ head
|
|||||||
link(rel='author' href='/humans.txt')
|
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.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="/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="/links/atom.xml" title="Garrett's Bookmarks (Atom)" type="application/atom+xml")
|
||||||
link(rel="alternate" href="/feed/rss.xml" title="Garrett Mills - Posts & Updates" type="application/rss+xml")
|
link(rel="alternate" href="/links/rss2.xml" title="Garrett's Bookmarks (RSS)" 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/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='apple-touch-icon' sizes='180x180' href=asset('favicon/apple-touch-icon.png'))
|
||||||
link(rel='manifest' href=asset('favicon/site.webmanifest'))
|
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>`
|
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 ) {
|
for ( const url of link.urls ) {
|
||||||
mmLines.push(`<li class="markmark link-url"><a href="${url}" target="_blank">${url}</a></li>`)
|
mmLines.push(`<li class="markmark link-url"><a href="${url}" target="_blank">${url}</a></li>`)
|
||||||
|
@ -1,14 +1,16 @@
|
|||||||
import {isNamedSection, MarkMark} from './types'
|
import {isNamedSection, MarkMark} from './types'
|
||||||
|
|
||||||
export class MarkMarkRenderer {
|
export class MarkMarkRenderer {
|
||||||
public render(mm: MarkMark): string {
|
public render(mm: MarkMark, frontmatter: boolean = true): string {
|
||||||
let mmLines: string[] = ['\n']
|
let mmLines: string[] = ['\n']
|
||||||
|
|
||||||
// Write the frontmatter
|
// Write the frontmatter
|
||||||
|
if ( frontmatter ) {
|
||||||
mmLines.push(`[//]: #(markmark-syntax: ${mm.frontmatter.syntax})`)
|
mmLines.push(`[//]: #(markmark-syntax: ${mm.frontmatter.syntax})`)
|
||||||
if (mm.frontmatter.authorName) mmLines.push(`[//]: #(markmark-author-name: ${mm.frontmatter.authorName})`)
|
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.authorEmail) mmLines.push(`[//]: #(markmark-author-email: ${mm.frontmatter.authorEmail})`)
|
||||||
if (mm.frontmatter.authorHref) mmLines.push(`[//]: #(markmark-author-href: ${mm.frontmatter.authorHref})`)
|
if (mm.frontmatter.authorHref) mmLines.push(`[//]: #(markmark-author-href: ${mm.frontmatter.authorHref})`)
|
||||||
|
}
|
||||||
|
|
||||||
for ( const section of mm.sections ) {
|
for ( const section of mm.sections ) {
|
||||||
mmLines.push('\n')
|
mmLines.push('\n')
|
||||||
@ -23,6 +25,11 @@ export class MarkMarkRenderer {
|
|||||||
|
|
||||||
for ( const link of section.links ) {
|
for ( const link of section.links ) {
|
||||||
let linkTitle = `- ${link.title}`
|
let linkTitle = `- ${link.title}`
|
||||||
|
|
||||||
|
if ( link.date ) {
|
||||||
|
linkTitle += ` (${this.formatDate(link.date)})`
|
||||||
|
}
|
||||||
|
|
||||||
if ( link.tags.length ) {
|
if ( link.tags.length ) {
|
||||||
linkTitle += ` ${link.tags.map(x => '#' + x).join(' ')}`
|
linkTitle += ` ${link.tags.map(x => '#' + x).join(' ')}`
|
||||||
}
|
}
|
||||||
@ -37,4 +44,11 @@ export class MarkMarkRenderer {
|
|||||||
|
|
||||||
return mmLines.join('\n')
|
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 * as marked from 'marked'
|
||||||
import {FrontMatter, isNamedSection, Link, MarkMark, Section} from './types'
|
import {FrontMatter, isNamedSection, Link, MarkMark, Section} from './types'
|
||||||
|
import * as crypto from 'crypto'
|
||||||
|
|
||||||
export class Parser {
|
export class Parser {
|
||||||
public parse(content: string): MarkMark {
|
public parse(content: string): MarkMark {
|
||||||
@ -65,6 +66,7 @@ export class Parser {
|
|||||||
currentLink = {
|
currentLink = {
|
||||||
title,
|
title,
|
||||||
date,
|
date,
|
||||||
|
hash: crypto.createHash('sha256').update(token.text).digest('hex'),
|
||||||
tags: this.parseTags(token.text),
|
tags: this.parseTags(token.text),
|
||||||
urls: [],
|
urls: [],
|
||||||
}
|
}
|
||||||
|
@ -20,6 +20,7 @@ export const isNamedSection = (what: Section): what is NamedSection =>
|
|||||||
hasOwnProperty(what, 'title') && (typeof what.title === 'string')
|
hasOwnProperty(what, 'title') && (typeof what.title === 'string')
|
||||||
|
|
||||||
export type Link = {
|
export type Link = {
|
||||||
|
hash: string,
|
||||||
title: string,
|
title: string,
|
||||||
date?: Date,
|
date?: Date,
|
||||||
tags: string[],
|
tags: string[],
|
||||||
|
Loading…
Reference in New Issue
Block a user