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:
Garrett Mills 2024-12-31 13:50:54 -05:00
parent 832190f875
commit b5450c6100
9 changed files with 153 additions and 10 deletions

View File

@ -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')
}
} }

View File

@ -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)

View File

@ -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

View File

@ -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'))

View 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}`
}
}

View File

@ -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>`)

View File

@ -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}`;
}
} }

View File

@ -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: [],
} }

View File

@ -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[],