Add initial MarkMark spec and integrate my own links.mark.md file
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
This commit is contained in:
parent
a6e1819d2d
commit
575a253651
@ -3,6 +3,7 @@ import { env } from '@extollo/lib'
|
|||||||
export interface ColorPalette {
|
export interface ColorPalette {
|
||||||
displayName: string
|
displayName: string
|
||||||
background: string
|
background: string
|
||||||
|
textVariant: 'light' | 'dark'
|
||||||
backgroundOffset: string
|
backgroundOffset: string
|
||||||
backgroundOffset2: string
|
backgroundOffset2: string
|
||||||
hero: string
|
hero: string
|
||||||
@ -27,6 +28,7 @@ export default {
|
|||||||
colors: {
|
colors: {
|
||||||
lostInTheStars: {
|
lostInTheStars: {
|
||||||
displayName: "Lost in the Stars",
|
displayName: "Lost in the Stars",
|
||||||
|
textVariant: 'light',
|
||||||
background: '#4a113c',
|
background: '#4a113c',
|
||||||
backgroundOffset: 'rgba(178, 127, 170, 0.1)',
|
backgroundOffset: 'rgba(178, 127, 170, 0.1)',
|
||||||
backgroundOffset2: 'rgba(178, 127, 170, 0.2)',
|
backgroundOffset2: 'rgba(178, 127, 170, 0.2)',
|
||||||
@ -43,6 +45,7 @@ export default {
|
|||||||
},
|
},
|
||||||
tanOrangeAndRed: {
|
tanOrangeAndRed: {
|
||||||
displayName: "Tan, Orange, & Red",
|
displayName: "Tan, Orange, & Red",
|
||||||
|
textVariant: 'dark',
|
||||||
background: '#f8e4bf',
|
background: '#f8e4bf',
|
||||||
backgroundOffset: 'rgb(243, 210, 147, 0.4)',
|
backgroundOffset: 'rgb(243, 210, 147, 0.4)',
|
||||||
backgroundOffset2: 'rgb(111, 71, 46, 0.15)',
|
backgroundOffset2: 'rgb(111, 71, 46, 0.15)',
|
||||||
@ -59,6 +62,7 @@ export default {
|
|||||||
},
|
},
|
||||||
blueAndTan: {
|
blueAndTan: {
|
||||||
displayName: "Blue & Tan",
|
displayName: "Blue & Tan",
|
||||||
|
textVariant: 'light',
|
||||||
background: '#052653',
|
background: '#052653',
|
||||||
backgroundOffset: 'rgba(27, 111, 145, 0.3)',
|
backgroundOffset: 'rgba(27, 111, 145, 0.3)',
|
||||||
backgroundOffset2: 'rgba(127, 167, 158, 0.15)',
|
backgroundOffset2: 'rgba(127, 167, 158, 0.15)',
|
||||||
@ -75,6 +79,7 @@ export default {
|
|||||||
},
|
},
|
||||||
teals: {
|
teals: {
|
||||||
displayName: "Teals",
|
displayName: "Teals",
|
||||||
|
textVariant: 'dark',
|
||||||
background: '#c6c2b9',
|
background: '#c6c2b9',
|
||||||
backgroundOffset: 'rgba(150, 171, 162, 0.3)',
|
backgroundOffset: 'rgba(150, 171, 162, 0.3)',
|
||||||
backgroundOffset2: 'rgba(150, 171, 162, 0.7)',
|
backgroundOffset2: 'rgba(150, 171, 162, 0.7)',
|
||||||
@ -91,6 +96,7 @@ export default {
|
|||||||
},
|
},
|
||||||
redAndGold: {
|
redAndGold: {
|
||||||
displayName: "Red & Gold",
|
displayName: "Red & Gold",
|
||||||
|
textVariant: 'light',
|
||||||
background: '#510c00',
|
background: '#510c00',
|
||||||
backgroundOffset: 'rgba(111, 42, 30, 0.4)',
|
backgroundOffset: 'rgba(111, 42, 30, 0.4)',
|
||||||
backgroundOffset2: 'rgba(111, 42, 30, 1)',
|
backgroundOffset2: 'rgba(111, 42, 30, 1)',
|
||||||
@ -107,6 +113,7 @@ export default {
|
|||||||
},
|
},
|
||||||
mashGreen: {
|
mashGreen: {
|
||||||
displayName: "M*A*S*H Green",
|
displayName: "M*A*S*H Green",
|
||||||
|
textVariant: 'light',
|
||||||
background: '#202318',
|
background: '#202318',
|
||||||
backgroundOffset: 'rgba(46, 41, 22, 0.5)',
|
backgroundOffset: 'rgba(46, 41, 22, 0.5)',
|
||||||
backgroundOffset2: 'rgb(105, 75, 1, 0.3)',
|
backgroundOffset2: 'rgb(105, 75, 1, 0.3)',
|
||||||
@ -123,6 +130,7 @@ export default {
|
|||||||
},
|
},
|
||||||
purpleAndWhite: {
|
purpleAndWhite: {
|
||||||
displayName: "Purple & White",
|
displayName: "Purple & White",
|
||||||
|
textVariant: 'light',
|
||||||
background: '#6669aa',
|
background: '#6669aa',
|
||||||
backgroundOffset: 'rgba(152, 155, 220, 0.4)',
|
backgroundOffset: 'rgba(152, 155, 220, 0.4)',
|
||||||
backgroundOffset2: 'rgba(81, 84, 143, 0.6)',
|
backgroundOffset2: 'rgba(81, 84, 143, 0.6)',
|
||||||
@ -139,6 +147,7 @@ export default {
|
|||||||
},
|
},
|
||||||
americana: {
|
americana: {
|
||||||
displayName: "Americana",
|
displayName: "Americana",
|
||||||
|
textVariant: 'dark',
|
||||||
background: '#f6f4f3',
|
background: '#f6f4f3',
|
||||||
backgroundOffset: 'rgba(120, 153, 185, 0.1)',
|
backgroundOffset: 'rgba(120, 153, 185, 0.1)',
|
||||||
backgroundOffset2: 'rgba(120, 153, 185, 0.2)',
|
backgroundOffset2: 'rgba(120, 153, 185, 0.2)',
|
||||||
@ -155,6 +164,7 @@ export default {
|
|||||||
},
|
},
|
||||||
ubuntu: {
|
ubuntu: {
|
||||||
displayName: "Ubuntu",
|
displayName: "Ubuntu",
|
||||||
|
textVariant: 'light',
|
||||||
background: '#333333',
|
background: '#333333',
|
||||||
backgroundOffset: 'rgba(51, 51, 51, 0.4)',
|
backgroundOffset: 'rgba(51, 51, 51, 0.4)',
|
||||||
backgroundOffset2: 'rgba(51, 51, 51, 0.6)',
|
backgroundOffset2: 'rgba(51, 51, 51, 0.6)',
|
||||||
@ -171,6 +181,7 @@ export default {
|
|||||||
},
|
},
|
||||||
mintMono: {
|
mintMono: {
|
||||||
displayName: "Mint Mono",
|
displayName: "Mint Mono",
|
||||||
|
textVariant: 'light',
|
||||||
background: '#6b9080',
|
background: '#6b9080',
|
||||||
backgroundOffset: 'rgba(164, 195, 178, 0.2)',
|
backgroundOffset: 'rgba(164, 195, 178, 0.2)',
|
||||||
backgroundOffset2: 'rgba(51, 51, 51, 0.6)',
|
backgroundOffset2: 'rgba(51, 51, 51, 0.6)',
|
||||||
@ -187,6 +198,7 @@ export default {
|
|||||||
},
|
},
|
||||||
abyss: {
|
abyss: {
|
||||||
displayName: "Abyss",
|
displayName: "Abyss",
|
||||||
|
textVariant: 'light',
|
||||||
background: '#010A19',
|
background: '#010A19',
|
||||||
backgroundOffset: 'rgba(1, 10, 25, 0.2)',
|
backgroundOffset: 'rgba(1, 10, 25, 0.2)',
|
||||||
backgroundOffset2: 'rgba(1, 10, 25, 0.6)',
|
backgroundOffset2: 'rgba(1, 10, 25, 0.6)',
|
||||||
@ -203,6 +215,7 @@ export default {
|
|||||||
},
|
},
|
||||||
blackIsBack: {
|
blackIsBack: {
|
||||||
displayName: 'Black Is The New Black',
|
displayName: 'Black Is The New Black',
|
||||||
|
textVariant: 'dark',
|
||||||
background: '#e4e4e4',
|
background: '#e4e4e4',
|
||||||
backgroundOffset: 'rgba(188, 188, 188, 0.2)',
|
backgroundOffset: 'rgba(188, 188, 188, 0.2)',
|
||||||
backgroundOffset2: 'rgba(188, 188, 188, 0.6)',
|
backgroundOffset2: 'rgba(188, 188, 188, 0.6)',
|
||||||
@ -219,6 +232,7 @@ export default {
|
|||||||
},
|
},
|
||||||
noir: {
|
noir: {
|
||||||
displayName: "Noir",
|
displayName: "Noir",
|
||||||
|
textVariant: 'light',
|
||||||
background: '#2a333f',
|
background: '#2a333f',
|
||||||
backgroundOffset: 'rgba(56, 53, 60, 0.3)',
|
backgroundOffset: 'rgba(56, 53, 60, 0.3)',
|
||||||
backgroundOffset2: 'rgb(45, 80, 96, 0.2)',
|
backgroundOffset2: 'rgb(45, 80, 96, 0.2)',
|
||||||
|
@ -10,7 +10,7 @@ import {
|
|||||||
file,
|
file,
|
||||||
Application,
|
Application,
|
||||||
make,
|
make,
|
||||||
Valid, Logging, api, Session,
|
Valid, Logging, api, Session, plaintext,
|
||||||
} from '@extollo/lib'
|
} from '@extollo/lib'
|
||||||
import {WorkItem} from '../../models/WorkItem.model'
|
import {WorkItem} from '../../models/WorkItem.model'
|
||||||
import {FeedPost} from '../../models/FeedPost.model'
|
import {FeedPost} from '../../models/FeedPost.model'
|
||||||
@ -156,6 +156,32 @@ export class Home extends Controller {
|
|||||||
.catch(e => this.logging.error(e))
|
.catch(e => this.logging.error(e))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getThemeCSSFile() {
|
||||||
|
let key = String(this.request.input('themeKey') || '')
|
||||||
|
if ( key.endsWith('.css') ) key = key.substring(0, key.length - 4)
|
||||||
|
const themes = this.config.get('app.colors') as Record<string, ColorPalette>
|
||||||
|
const themeKeys = Object.keys(themes)
|
||||||
|
const theme = themes[key] || themes[themeKeys[0]]
|
||||||
|
const themeCSS = `
|
||||||
|
/* Theme: ${theme.displayName} */
|
||||||
|
:root {
|
||||||
|
--c-background: ${theme.background};
|
||||||
|
--c-background-offset: ${theme.backgroundOffset};
|
||||||
|
--c-background-offset-2: ${theme.backgroundOffset2};
|
||||||
|
--c-hero: ${theme.hero};
|
||||||
|
--c-font: ${theme.font};
|
||||||
|
--c-font-muted: ${theme.fontMuted};
|
||||||
|
--c-box: ${theme.box};
|
||||||
|
--c-link: ${theme.link};
|
||||||
|
--c-noise-size: ${theme.noiseSize};
|
||||||
|
--c-line-1: ${theme.line1};
|
||||||
|
--c-line-2: ${theme.line2};
|
||||||
|
--c-line-3: ${theme.line3};
|
||||||
|
}
|
||||||
|
`
|
||||||
|
return plaintext(themeCSS).contentType('text/css')
|
||||||
|
}
|
||||||
|
|
||||||
public getThemeCSS(): any {
|
public getThemeCSS(): any {
|
||||||
const themes = this.config.get('app.colors') as Record<string, ColorPalette>
|
const themes = this.config.get('app.colors') as Record<string, ColorPalette>
|
||||||
const themeKeys = Object.keys(themes)
|
const themeKeys = Object.keys(themes)
|
||||||
@ -178,12 +204,23 @@ export class Home extends Controller {
|
|||||||
--c-line-3: ${theme.line3};
|
--c-line-3: ${theme.line3};
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
|
const themeStylesheets = themeKeys
|
||||||
|
.filter(key => key !== themeName)
|
||||||
|
.map(key => ({
|
||||||
|
key,
|
||||||
|
displayName: themes[key].displayName,
|
||||||
|
url: `/theme/${key}.css`,
|
||||||
|
}))
|
||||||
|
|
||||||
return {
|
return {
|
||||||
themeName,
|
themeName,
|
||||||
themeCSS,
|
themeCSS: '',
|
||||||
themeDisplayName: theme.displayName,
|
themeDisplayName: theme.displayName,
|
||||||
themeKeys,
|
themeKeys,
|
||||||
|
themeStylesheets,
|
||||||
themeRecord: theme,
|
themeRecord: theme,
|
||||||
|
textIsLight: theme.textVariant === 'light',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
50
src/app/http/controllers/MarkMark.controller.ts
Normal file
50
src/app/http/controllers/MarkMark.controller.ts
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
import {appPath, Controller, Inject, Injectable, plaintext, view} from '@extollo/lib'
|
||||||
|
import {Home} from './Home.controller'
|
||||||
|
import * as marked from 'marked'
|
||||||
|
import {MarkMarkService} from '../../services/MarkMark.service'
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class MarkMark extends Controller {
|
||||||
|
@Inject()
|
||||||
|
protected readonly markmark!: MarkMarkService
|
||||||
|
|
||||||
|
public welcome() {
|
||||||
|
const home = <Home> this.make(Home)
|
||||||
|
return view('markmark:welcome', {
|
||||||
|
...home.getThemeCSS(),
|
||||||
|
title: 'MarkMark (v1.0)',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private cachedStandard?: string
|
||||||
|
|
||||||
|
public async standard() {
|
||||||
|
if ( !this.cachedStandard ) {
|
||||||
|
const path = appPath('resources', 'markmark', 'standard.md')
|
||||||
|
const content = await path.read()
|
||||||
|
this.cachedStandard = marked.marked(content)
|
||||||
|
}
|
||||||
|
|
||||||
|
const home = <Home> this.make(Home)
|
||||||
|
return view('markmark:standard', {
|
||||||
|
...home.getThemeCSS(),
|
||||||
|
title: 'Spec: MarkMark (v1.0)',
|
||||||
|
contentHtml: this.cachedStandard,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
public async linksHtml() {
|
||||||
|
const home = <Home> this.make(Home)
|
||||||
|
return view('links', {
|
||||||
|
...home.getThemeCSS(),
|
||||||
|
title: 'Bookmarks',
|
||||||
|
contentHtml: await this.markmark.getLinksHTML(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
public async linksMarkMark() {
|
||||||
|
return plaintext(await this.markmark.getLinksMM())
|
||||||
|
.contentType('text/markdown;variant=markmark')
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -10,6 +10,7 @@ import {ValidContactForm} from '../middlewares/parameters/ValidContactForm.middl
|
|||||||
import {RateLimit} from '../middlewares/RateLimit.middleware'
|
import {RateLimit} from '../middlewares/RateLimit.middleware'
|
||||||
import {SiteTheme} from '../middlewares/SiteTheme.middleware'
|
import {SiteTheme} from '../middlewares/SiteTheme.middleware'
|
||||||
import {Blog} from '../controllers/Blog.controller'
|
import {Blog} from '../controllers/Blog.controller'
|
||||||
|
import {MarkMark} from '../controllers/MarkMark.controller'
|
||||||
|
|
||||||
Route.endpoint('options', '**')
|
Route.endpoint('options', '**')
|
||||||
.handledBy(() => api.one({}))
|
.handledBy(() => api.one({}))
|
||||||
@ -98,6 +99,7 @@ Route
|
|||||||
.calls<Snippets>(Snippets, snippets => snippets.viewSnippet)
|
.calls<Snippets>(Snippets, snippets => snippets.viewSnippet)
|
||||||
|
|
||||||
Route.any('/go/:short')
|
Route.any('/go/:short')
|
||||||
|
.pre(SiteTheme)
|
||||||
.calls<GoLinks>(GoLinks, go => go.launch)
|
.calls<GoLinks>(GoLinks, go => go.launch)
|
||||||
|
|
||||||
Route.get('/favicon.ico')
|
Route.get('/favicon.ico')
|
||||||
@ -126,6 +128,27 @@ Route
|
|||||||
.calls<Feed>(Feed, feed => feed.json)
|
.calls<Feed>(Feed, feed => feed.json)
|
||||||
.alias('feed.json')
|
.alias('feed.json')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
Route.group('/markmark', () => {
|
||||||
|
Route.get('/')
|
||||||
|
.pre(SiteTheme)
|
||||||
|
.calls<MarkMark>(MarkMark, m => m.welcome)
|
||||||
|
|
||||||
|
Route.get('/standard')
|
||||||
|
.pre(SiteTheme)
|
||||||
|
.calls<MarkMark>(MarkMark, m => m.standard)
|
||||||
|
})
|
||||||
|
|
||||||
|
Route.get('/theme/:themeKey')
|
||||||
|
.pre(SiteTheme)
|
||||||
|
.calls<Home>(Home, h => h.getThemeCSSFile)
|
||||||
|
|
||||||
|
Route.get('/links')
|
||||||
|
.pre(SiteTheme)
|
||||||
|
.calls<MarkMark>(MarkMark, m => m.linksHtml)
|
||||||
|
|
||||||
|
Route.get('/links.mark.md')
|
||||||
|
.calls<MarkMark>(MarkMark, m => m.linksMarkMark)
|
||||||
})
|
})
|
||||||
.pre(SessionAuthMiddleware)
|
.pre(SessionAuthMiddleware)
|
||||||
.pre(PageView)
|
.pre(PageView)
|
||||||
|
@ -452,6 +452,12 @@ footer.theme-stats li {
|
|||||||
color: var(--c-font-muted);
|
color: var(--c-font-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.inline-markmark-logo {
|
||||||
|
height: 16pt;
|
||||||
|
margin-bottom: -3px;
|
||||||
|
opacity: 0.75;
|
||||||
|
}
|
||||||
|
|
||||||
.button-links {
|
.button-links {
|
||||||
margin: 20px;
|
margin: 20px;
|
||||||
}
|
}
|
||||||
|
63
src/app/resources/assets/markmark-dark.svg
Normal file
63
src/app/resources/assets/markmark-dark.svg
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg:svg
|
||||||
|
width="208"
|
||||||
|
height="128"
|
||||||
|
viewBox="0 0 208 128"
|
||||||
|
version="1.1"
|
||||||
|
id="svg1"
|
||||||
|
sodipodi:docname="ink-markmark-dark.svg"
|
||||||
|
inkscape:version="1.3 (0e150ed6c4, 2023-07-21)"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg">
|
||||||
|
<svg:defs
|
||||||
|
id="defs1" />
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="namedview1"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#000000"
|
||||||
|
borderopacity="0.25"
|
||||||
|
inkscape:showpageshadow="2"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
inkscape:deskcolor="#d1d1d1"
|
||||||
|
inkscape:zoom="6.4081552"
|
||||||
|
inkscape:cx="85.281954"
|
||||||
|
inkscape:cy="73.109965"
|
||||||
|
inkscape:window-width="2544"
|
||||||
|
inkscape:window-height="1331"
|
||||||
|
inkscape:window-x="26"
|
||||||
|
inkscape:window-y="23"
|
||||||
|
inkscape:window-maximized="0"
|
||||||
|
inkscape:current-layer="svg1" />
|
||||||
|
<svg:rect
|
||||||
|
width="198"
|
||||||
|
height="118"
|
||||||
|
x="5"
|
||||||
|
y="5"
|
||||||
|
ry="10"
|
||||||
|
stroke="#000000"
|
||||||
|
stroke-width="10"
|
||||||
|
fill="none"
|
||||||
|
id="rect1" />
|
||||||
|
<svg:path
|
||||||
|
d="M 30,98 V 30 H 50 L 70,55 90,30 h 20 V 98 H 90 V 59 L 70,84 50,59 v 39 z"
|
||||||
|
id="path1"
|
||||||
|
sodipodi:nodetypes="ccccccccccccc" />
|
||||||
|
<script />
|
||||||
|
<svg:rect
|
||||||
|
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:21.7013;stroke-linecap:round"
|
||||||
|
id="rect2"
|
||||||
|
width="46.446552"
|
||||||
|
height="45.751724"
|
||||||
|
x="130.91896"
|
||||||
|
y="30" />
|
||||||
|
<svg:path
|
||||||
|
style="fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:1;stroke-linecap:round;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="M 131.41896,75.742009 V 97.990285 L 154.64223,75.742009 Z"
|
||||||
|
id="path3" />
|
||||||
|
<svg:path
|
||||||
|
style="fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:1;stroke-linecap:round;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="M 176.86551,75.751724 V 98 L 153.64224,75.751724 Z"
|
||||||
|
id="path3-6" />
|
||||||
|
</svg:svg>
|
After Width: | Height: | Size: 1.9 KiB |
65
src/app/resources/assets/markmark-light.svg
Normal file
65
src/app/resources/assets/markmark-light.svg
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg:svg
|
||||||
|
width="208"
|
||||||
|
height="128"
|
||||||
|
viewBox="0 0 208 128"
|
||||||
|
version="1.1"
|
||||||
|
id="svg1"
|
||||||
|
sodipodi:docname="inkmarkmark-light.svg"
|
||||||
|
inkscape:version="1.3 (0e150ed6c4, 2023-07-21)"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg">
|
||||||
|
<svg:defs
|
||||||
|
id="defs1" />
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="namedview1"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#000000"
|
||||||
|
borderopacity="0.25"
|
||||||
|
inkscape:showpageshadow="2"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
inkscape:deskcolor="#d1d1d1"
|
||||||
|
inkscape:zoom="6.4081552"
|
||||||
|
inkscape:cx="85.281954"
|
||||||
|
inkscape:cy="73.109965"
|
||||||
|
inkscape:window-width="2544"
|
||||||
|
inkscape:window-height="1331"
|
||||||
|
inkscape:window-x="26"
|
||||||
|
inkscape:window-y="23"
|
||||||
|
inkscape:window-maximized="0"
|
||||||
|
inkscape:current-layer="svg1" />
|
||||||
|
<svg:rect
|
||||||
|
width="198"
|
||||||
|
height="118"
|
||||||
|
x="5"
|
||||||
|
y="5"
|
||||||
|
ry="10"
|
||||||
|
stroke="#000"
|
||||||
|
stroke-width="10"
|
||||||
|
fill="none"
|
||||||
|
id="rect1"
|
||||||
|
style="stroke:#ffffff;stroke-opacity:1" />
|
||||||
|
<svg:path
|
||||||
|
d="M 30,98 V 30 H 50 L 70,55 90,30 h 20 V 98 H 90 V 59 L 70,84 50,59 v 39 z"
|
||||||
|
id="path1"
|
||||||
|
sodipodi:nodetypes="ccccccccccccc"
|
||||||
|
style="fill:#fcfcfc;fill-opacity:1" />
|
||||||
|
<script />
|
||||||
|
<svg:rect
|
||||||
|
style="fill:#fcfcfc;fill-opacity:1;stroke:none;stroke-width:21.7013;stroke-linecap:round"
|
||||||
|
id="rect2"
|
||||||
|
width="46.446552"
|
||||||
|
height="45.751724"
|
||||||
|
x="130.91896"
|
||||||
|
y="30" />
|
||||||
|
<svg:path
|
||||||
|
style="fill:#fcfcfc;fill-opacity:1;stroke:#ffffff;stroke-width:1;stroke-linecap:round;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="M 131.41896,75.742009 V 97.990285 L 154.64223,75.742009 Z"
|
||||||
|
id="path3" />
|
||||||
|
<svg:path
|
||||||
|
style="fill:#fcfcfc;fill-opacity:1;stroke:#ffffff;stroke-width:1;stroke-linecap:round;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="M 176.86551,75.751724 V 98 L 153.64224,75.751724 Z"
|
||||||
|
id="path3-6" />
|
||||||
|
</svg:svg>
|
After Width: | Height: | Size: 2.0 KiB |
54
src/app/resources/markmark/links.mark.md
Normal file
54
src/app/resources/markmark/links.mark.md
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
|
||||||
|
[//]: #(markmark-syntax: v1)
|
||||||
|
[//]: #(markmark-author-name: Garrett Mills)
|
||||||
|
[//]: #(markmark-author-email: shout@garrettmills.dev)
|
||||||
|
[//]: #(markmark-author-href: https://garrettmills.dev/)
|
||||||
|
|
||||||
|
# Games
|
||||||
|
|
||||||
|
- OpenTTD - open-source implementation of Transport Tycoon Deluxe
|
||||||
|
- https://www.openttd.org/
|
||||||
|
- Dwarf Fortress
|
||||||
|
- https://www.bay12games.com/dwarves/
|
||||||
|
|
||||||
|
# Recipes & Cooking
|
||||||
|
|
||||||
|
- Homemade Pumpkin Pie Filling
|
||||||
|
- https://unsophisticook.com/homemade-pumpkin-pie-filling/
|
||||||
|
|
||||||
|
# Tech Projects
|
||||||
|
|
||||||
|
Tech projects that I found interesting, funny, or wanted to experiment with later.
|
||||||
|
|
||||||
|
- ScratchDB - Open-Source Snowflake on ClickHouse
|
||||||
|
- https://www.scratchdb.com/
|
||||||
|
- https://github.com/scratchdata/ScratchDB
|
||||||
|
- COBOL for GCC Development #lang
|
||||||
|
- https://cobolworx.com/pages/cobforgcc.html
|
||||||
|
|
||||||
|
# Technical Guides
|
||||||
|
|
||||||
|
- EV Code Certificates + Automated Builds for Windows
|
||||||
|
- https://medium.com/@joshualipson/ev-code-certificates-automated-builds-for-windows-6100fb8e8be6
|
||||||
|
|
||||||
|
# Blogs & Posts
|
||||||
|
|
||||||
|
- Dan Luu #dev
|
||||||
|
- https://danluu.com/
|
||||||
|
- https://danluu.com/everything-is-broken/
|
||||||
|
- The Case of a Curious SQL Query - Justing Jaffray #dev
|
||||||
|
- https://buttondown.email/jaffray/archive/the-case-of-a-curious-sql-query/
|
||||||
|
- An Aborted Experiment with Server Swift - flak Blog #dev
|
||||||
|
- https://flak.tedunangst.com/post/an-aborted-experiment-with-server-swift
|
||||||
|
- The Grug Brained Developer #dev
|
||||||
|
- https://grugbrain.dev/
|
||||||
|
- Code Rant - The Configuration Complexity Clock #dev
|
||||||
|
- https://mikehadlow.blogspot.com/2012/05/configuration-complexity-clock.html
|
||||||
|
- Mark L. Irons - Patterns for Personal Web Sites #dev #archive
|
||||||
|
- https://web.archive.org/web/20190904131208/http://www.rdrop.com/~half/Creations/Writings/Web.patterns/index.html
|
||||||
|
- https://web.archive.org/web/20190826113439/http://www.rdrop.com/~half/Creations/Writings/Web.patterns/standard.header.and.footer.html
|
||||||
|
- https://web.archive.org/web/20200107155946/http://www.rdrop.com/~half/Creations/Writings/Web.patterns/history.page.html
|
||||||
|
- https://web.archive.org/web/20200107162931/http://www.rdrop.com/~half/Creations/Writings/Web.patterns/index.pages.html
|
||||||
|
- https://web.archive.org/web/20230314204244/http://www.rdrop.com/~half/Creations/Writings/Web.patterns/gift.to.the.community.html
|
||||||
|
- https://web.archive.org/web/20211102185515/http://www.rdrop.com/~half/Creations/Writings/Web.patterns/downloadable.weblet.html
|
||||||
|
- https://web.archive.org/web/20220120050151/http://www.rdrop.com/~half/Creations/Writings/Web.patterns/site.map.html
|
147
src/app/resources/markmark/standard.md
Normal file
147
src/app/resources/markmark/standard.md
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
# Standard MarkMark (v1.0)
|
||||||
|
|
||||||
|
**MarkMark is an extremely simple, standard, plain-text format for collecting bookmarks in such a way that they can be shared and federated.**
|
||||||
|
|
||||||
|
## File Format & Rel-Links
|
||||||
|
|
||||||
|
MarkMark files SHOULD be stored as plain-text Markdown files with the extension `.mark.md`.
|
||||||
|
|
||||||
|
Example: `john-doe.mark.md`
|
||||||
|
|
||||||
|
You MAY refer to a MarkMark file in the `<head>` tag of an HTML document as you would with, for example, and RSS feed using the media type `text/markdown;variant=markmark`. This media type is derived from [RFC 7763](https://www.rfc-editor.org/rfc/rfc7763).
|
||||||
|
|
||||||
|
Example:
|
||||||
|
```html
|
||||||
|
<link rel="alternate" href="/john-doe.mark.md" title="My Bookmarks" type="text/markdown;variant=markmark"/>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Syntax
|
||||||
|
|
||||||
|
MarkMark is a subset of Markdown (specifically [CommonMark](https://spec.commonmark.org/)) designed to render properly in vanilla Markdown clients.
|
||||||
|
|
||||||
|
```text
|
||||||
|
MARKMARK := FRONTMATTER \n LINES
|
||||||
|
|
||||||
|
LINES :=
|
||||||
|
LINE LINES
|
||||||
|
| LINE EOF
|
||||||
|
| EOF
|
||||||
|
|
||||||
|
LINE := SECTION | LINK
|
||||||
|
```
|
||||||
|
|
||||||
|
### Frontmatter
|
||||||
|
|
||||||
|
```txt
|
||||||
|
FRONTMATTER := \n [//]: # (FRONTMATTER_VALUE)
|
||||||
|
|
||||||
|
FRONTMATTER_VALUE :=
|
||||||
|
markmark-syntax: v1
|
||||||
|
| markmark-author-name: STRING
|
||||||
|
| markmark-author-email: STRING
|
||||||
|
| markmark-author-href: STRING
|
||||||
|
```
|
||||||
|
|
||||||
|
MarkMark uses a subset of Markdown Link Reference Definitions (CommonMark §4.7) to add MarkMark-specific metadata to the file. These values MUST appear at the top of the file. These values SHOULD be provided. The first FrontMatter item MUST be prefixed with a newline character (`\n`). You MAY prefix subsequent FrontMatter items with a newline character (`\n`). You MUST suffix the last FrontMatter item with a newline character (`\n`).
|
||||||
|
|
||||||
|
#### Supported Properties
|
||||||
|
|
||||||
|
- `markmark-syntax` - The MarkMark spec version (supported values: `v1`)
|
||||||
|
- `markmark-author-name` - The name of the author of this bookmark collection
|
||||||
|
- `markmark-author-email` - The email address of the author
|
||||||
|
- `markmark-author-href` - The URL of the author's website
|
||||||
|
|
||||||
|
#### Example
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
|
||||||
|
[//]: # (markmark-syntax: v1)
|
||||||
|
[//]: # (markmark-author-name: John Doe)
|
||||||
|
[//]: # (markmark-author-email: john.doe@example.com)
|
||||||
|
[//]: # (markmark-author-href: https://john-doe.example.com/)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Sections
|
||||||
|
|
||||||
|
```text
|
||||||
|
SECTION := # STRING
|
||||||
|
| # STRING \n STRING
|
||||||
|
```
|
||||||
|
|
||||||
|
In MarkMark, authors MAY organize their bookmarks into sections. These sections MUST be indicated using a Markdown Heading 1 syntax (CommonMark §4.2) and MAY be followed by a Markdown Paragraph (CommonMark §4.8) describing the section.
|
||||||
|
|
||||||
|
#### Example
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
# Technical Blogs
|
||||||
|
|
||||||
|
This is a description of the Technical Blogs section in my MarkMark file.
|
||||||
|
```
|
||||||
|
|
||||||
|
### Links
|
||||||
|
|
||||||
|
```text
|
||||||
|
LINK :=
|
||||||
|
- STRING TAGS\nINDENT HREFS
|
||||||
|
|
||||||
|
TAGS :=
|
||||||
|
TAG TAGS
|
||||||
|
| EOF
|
||||||
|
|
||||||
|
TAG := #SLUG
|
||||||
|
|
||||||
|
INDENT := ␣␣␣␣ | \t
|
||||||
|
|
||||||
|
HREFS :=
|
||||||
|
- URL
|
||||||
|
| - [URL](URL)
|
||||||
|
```
|
||||||
|
|
||||||
|
Links are individual entries in a bookmark collection and represent a bookmark title and one or more URLs relevant to that bookmark. These URLs SHOULD be limited to less than 5.
|
||||||
|
|
||||||
|
A link is a Markdown Bullet List item (CommonMark §5.2) with a sub-list of URLs. Each URL MUST be either a plain-text URL OR a Markdown Link (CommonMark §6.3). Markdown Link URLs MUST use the URL as the only text for the link.
|
||||||
|
|
||||||
|
#### Tags
|
||||||
|
|
||||||
|
Each link MAY have one or more tags associated with it. Tags provide an additional organizational scheme as they can be used to associate links across sections.
|
||||||
|
|
||||||
|
Tags are composed of a `#`-sign followed by a slug composed of alpha-numeric-underscore-dash `a-zA-Z0-9_\-` characters. Multiple tags MUST be separated by one or more space characters.
|
||||||
|
|
||||||
|
If a link has tags, those tags MUST appear at the end of the line containing the link's title, after one or more space characters.
|
||||||
|
|
||||||
|
#### Example
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
- My Blog #personal-site #blog
|
||||||
|
- https://john-doe.example.com/blog
|
||||||
|
- Jane's Blog #blogs
|
||||||
|
- https://jane-doe.example.com/blog
|
||||||
|
- Example.com
|
||||||
|
- [https://example.com](https://example.com)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Full Example
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
|
||||||
|
[//]: # (markmark-syntax: v1)
|
||||||
|
[//]: # (markmark-author-name: John Doe)
|
||||||
|
[//]: # (markmark-author-email: john.doe@example.com)
|
||||||
|
[//]: # (markmark-author-href: https://john-doe.example.com/)
|
||||||
|
|
||||||
|
# Doe-Family Sites
|
||||||
|
|
||||||
|
Here is a collection of personal blogs and homepages from members of the Doe family.
|
||||||
|
|
||||||
|
- My Blog #personal-site #blog
|
||||||
|
- https://john-doe.example.com/blog
|
||||||
|
- Jane's Blog #blogs
|
||||||
|
- https://jane-doe.example.com/blog
|
||||||
|
- Example.com
|
||||||
|
- [https://example.com](https://example.com)
|
||||||
|
```
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
[Standard MarkMark](https://garrettmills.dev/markmark) by [Garrett Mills](https://garrettmills.dev/) is marked with [CC0 1.0 Universal](http://creativecommons.org/publicdomain/zero/1.0?ref=chooser-v1).
|
||||||
|
|
30
src/app/resources/views/links.pug
Normal file
30
src/app/resources/views/links.pug
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
extends template_70s
|
||||||
|
|
||||||
|
block content
|
||||||
|
.container#top
|
||||||
|
.inner
|
||||||
|
.hero
|
||||||
|
.hero-box
|
||||||
|
h1 Garrett Mills
|
||||||
|
section#header
|
||||||
|
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>).
|
||||||
|
section#links !{contentHtml}
|
||||||
|
|
||||||
|
block append style
|
||||||
|
style.
|
||||||
|
section { margin-top: 0; padding-bottom: 0; }
|
||||||
|
|
||||||
|
h1 { font-size: 40pt; }
|
||||||
|
h2 { font-size: 30pt; }
|
||||||
|
h3 { font-size: 24pt; }
|
||||||
|
h4 { font-size: 18pt; }
|
||||||
|
|
||||||
|
#links h1 { font-size: 25pt; margin-top: 60px; }
|
||||||
|
|
||||||
|
*:not(li) > ul > li { margin-top: 20px; }
|
||||||
|
|
||||||
|
.markmark.link-tags { margin-left: 30px; }
|
||||||
|
.markmark.link-tag { color: var(--c-font-muted); }
|
28
src/app/resources/views/markmark/standard.pug
Normal file
28
src/app/resources/views/markmark/standard.pug
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
extends ../template_70s
|
||||||
|
|
||||||
|
block content
|
||||||
|
.container#top
|
||||||
|
.inner
|
||||||
|
h2(style='font-size: 26pt; padding-top: 20px') Garrett Mills
|
||||||
|
ul.inline-nav
|
||||||
|
li
|
||||||
|
a.button(href=named('home')) Home
|
||||||
|
li
|
||||||
|
a.button(href=route('/markmark')) About MarkMark
|
||||||
|
li
|
||||||
|
a.button(href=route('/links')) My Bookmarks
|
||||||
|
|
||||||
|
section#main !{contentHtml}
|
||||||
|
|
||||||
|
block append style
|
||||||
|
link(rel='stylesheet' href=asset('highlight/styles/' + themeRecord.highlightTheme + '.min.css'))
|
||||||
|
style.
|
||||||
|
h1 { font-size: 40pt; }
|
||||||
|
h2 { font-size: 30pt; }
|
||||||
|
h3 { font-size: 24pt; }
|
||||||
|
h4 { font-size: 18pt; }
|
||||||
|
|
||||||
|
block append script
|
||||||
|
script(src=asset('highlight/highlight.min.js'))
|
||||||
|
script.
|
||||||
|
hljs.highlightAll();
|
57
src/app/resources/views/markmark/welcome.pug
Normal file
57
src/app/resources/views/markmark/welcome.pug
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
extends ../template_70s
|
||||||
|
|
||||||
|
block content
|
||||||
|
.container#top
|
||||||
|
.inner
|
||||||
|
h2(style='font-size: 26pt; padding-top: 20px') Garrett Mills
|
||||||
|
ul.inline-nav
|
||||||
|
li
|
||||||
|
a.button(href=named('home')) Home
|
||||||
|
li
|
||||||
|
a.button(href=route('/markmark')) About MarkMark
|
||||||
|
li
|
||||||
|
a.button(href=route('/links')) My Bookmarks
|
||||||
|
|
||||||
|
section#main
|
||||||
|
img.markmark-mark(src=asset(textIsLight ? 'markmark-light.svg' : 'markmark-dark.svg') alt="MarkMark Logo")
|
||||||
|
h2(style='font-size: 40pt') Standard MarkMark (v1.0)
|
||||||
|
p MarkMark is a free (<a href="https://en.wikipedia.org/wiki/Free_as_in_Freedom" target="_blank">as in freedom</a>) bookmark format designed to be machine-readable and easy to use.
|
||||||
|
p The goal of MarkMark is to standardize "link sharing" pages to build connections between small websites on the Internet.
|
||||||
|
p You can view the full standard <a href="#{route('/markmark/standard')}">here</a>.
|
||||||
|
p
|
||||||
|
i MarkMark was inspired by the post <a href="https://www.marginalia.nu/log/19-website-discoverability-crisis/" target="_blank">The Small Website Discoverability Crisis</a> on the Marginalia blog.
|
||||||
|
|
||||||
|
h3 Example
|
||||||
|
pre
|
||||||
|
code.hljs.language-markdown
|
||||||
|
|
|
||||||
|
| [//]: #(markmark-syntax: v1)
|
||||||
|
| [//]: #(markmark-author-name: Garrett Mills)
|
||||||
|
| [//]: #(markmark-author-email: shout@garrettmills.dev)
|
||||||
|
| [//]: #(markmark-author-href: https://garrettmills.dev/)
|
||||||
|
|
|
||||||
|
| # My Sites
|
||||||
|
|
|
||||||
|
| Here are links to various sites created by yours truly.
|
||||||
|
|
|
||||||
|
| - Garrett Mills
|
||||||
|
| - https://garrettmills.dev/
|
||||||
|
|
|
||||||
|
| - My Code
|
||||||
|
| - https://code.garrettmills.dev/garrettmills
|
||||||
|
|
||||||
|
h3 Resources
|
||||||
|
ul
|
||||||
|
li
|
||||||
|
a(href=route('/markmark/standard')) Spec: Standard MarkMark (v1.0)
|
||||||
|
|
||||||
|
block append style
|
||||||
|
link(rel='stylesheet' href=asset('highlight/styles/' + themeRecord.highlightTheme + '.min.css'))
|
||||||
|
|
||||||
|
style.
|
||||||
|
.markmark-mark { width: 200px; margin-bottom: -60px; opacity: 0.75; }
|
||||||
|
|
||||||
|
block append script
|
||||||
|
script(src=asset('highlight/highlight.min.js'))
|
||||||
|
script.
|
||||||
|
hljs.highlightAll();
|
@ -24,6 +24,8 @@ block content
|
|||||||
br
|
br
|
||||||
br
|
br
|
||||||
|
|
||||||
|
p You can also change the theme using the little-known <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Alternative_style_sheets" target="_blank">alternate stylesheets</a> feature by going to View > Page Style in your browser.
|
||||||
|
|
||||||
h4 The Stars
|
h4 The Stars
|
||||||
p The homepage of this site features a constellation of 6 translucent stars which appears at the top of the page.
|
p The homepage of this site features a constellation of 6 translucent stars which appears at the top of the page.
|
||||||
p The positions of the stars are randomly generated based on the dimensions of the page. To discourage clustered/skewed constellations, the following criteria are used:
|
p The positions of the stars are randomly generated based on the dimensions of the page. To discourage clustered/skewed constellations, the following criteria are used:
|
||||||
|
@ -16,14 +16,21 @@ head
|
|||||||
title #{config('app.name', 'Garrett Mills')}
|
title #{config('app.name', 'Garrett Mills')}
|
||||||
|
|
||||||
block style
|
block style
|
||||||
style !{themeCSS}
|
|
||||||
script.
|
script.
|
||||||
window.glmdev = window.glmdev || {}
|
window.glmdev = window.glmdev || {}
|
||||||
window.glmdev.themeStats = window.glmdev.themeStats || []
|
window.glmdev.themeStats = window.glmdev.themeStats || []
|
||||||
window.glmdev.themeStats.push('Theme: !{themeDisplayName}')
|
window.glmdev.themeStats.push('Default Theme: !{themeDisplayName}')
|
||||||
link(rel='stylesheet' href=asset('main-70s.css'))
|
link(rel='stylesheet' href=asset('main-70s.css'))
|
||||||
|
|
||||||
|
link(rel='stylesheet' href=`/theme/${themeName}.css` title=themeDisplayName)
|
||||||
|
if themeStylesheets
|
||||||
|
each sheet in themeStylesheets
|
||||||
|
link(rel='alternate stylesheet' href=sheet.url title=sheet.displayName)
|
||||||
|
|
||||||
|
|
||||||
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" 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/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/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="/feed/json.json" title="Garrett Mills - Posts & Updates" type="application/feed+json")
|
||||||
@ -72,7 +79,7 @@ body
|
|||||||
.col
|
.col
|
||||||
ul.links
|
ul.links
|
||||||
li
|
li
|
||||||
a(href='https://static.garrettmills.dev/Resume.pdf' target='_blank') résumé
|
a(href='/links') bookmarks <img class="inline-markmark-logo" src="#{asset(textIsLight ? 'markmark-light.svg' : 'markmark-dark.svg')}"/>
|
||||||
li
|
li
|
||||||
a(href='/blog') blog
|
a(href='/blog') blog
|
||||||
li
|
li
|
||||||
|
40
src/app/services/MarkMark.service.ts
Normal file
40
src/app/services/MarkMark.service.ts
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import {appPath, Singleton} from '@extollo/lib'
|
||||||
|
import {MarkMark} from '../../markmark/types'
|
||||||
|
import {Parser} from '../../markmark/parser'
|
||||||
|
import {HtmlRenderer} from '../../markmark/html.renderer'
|
||||||
|
import {MarkMarkRenderer} from '../../markmark/markmark.renderer'
|
||||||
|
|
||||||
|
@Singleton()
|
||||||
|
export class MarkMarkService {
|
||||||
|
private cachedLinks?: MarkMark
|
||||||
|
private cachedLinksHTML?: string
|
||||||
|
private cachedLinksMM?: string
|
||||||
|
|
||||||
|
public async getLinks(): Promise<MarkMark> {
|
||||||
|
if ( this.cachedLinks ) {
|
||||||
|
return this.cachedLinks
|
||||||
|
}
|
||||||
|
|
||||||
|
const path = appPath('resources', 'markmark', 'links.mark.md')
|
||||||
|
const content = await path.read()
|
||||||
|
return (this.cachedLinks = (new Parser()).parse(content))
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getLinksHTML(): Promise<string> {
|
||||||
|
if ( this.cachedLinksHTML ) {
|
||||||
|
return this.cachedLinksHTML
|
||||||
|
}
|
||||||
|
|
||||||
|
const mm = await this.getLinks()
|
||||||
|
return (this.cachedLinksHTML = (new HtmlRenderer()).render(mm))
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getLinksMM(): Promise<string> {
|
||||||
|
if ( this.cachedLinksMM ) {
|
||||||
|
return this.cachedLinksMM
|
||||||
|
}
|
||||||
|
|
||||||
|
const mm = await this.getLinks()
|
||||||
|
return (this.cachedLinksMM = (new MarkMarkRenderer()).render(mm))
|
||||||
|
}
|
||||||
|
}
|
41
src/markmark/html.renderer.ts
Normal file
41
src/markmark/html.renderer.ts
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import {isNamedSection, MarkMark} from './types'
|
||||||
|
|
||||||
|
export class HtmlRenderer {
|
||||||
|
public render(mm: MarkMark): string {
|
||||||
|
let mmLines: string[] = []
|
||||||
|
|
||||||
|
for ( const section of mm.sections ) {
|
||||||
|
mmLines.push('<section class="markmark section">')
|
||||||
|
|
||||||
|
// if this section has a title/description, write those out
|
||||||
|
if ( isNamedSection(section) ) {
|
||||||
|
mmLines.push(`<h1 class="markmark section-title">${section.title}</h1>`)
|
||||||
|
if ( section.description ) {
|
||||||
|
mmLines.push(`<p class="markmark section-description">${section.description}</p>`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mmLines.push('<ul class="markmark section-list">')
|
||||||
|
|
||||||
|
for ( const link of section.links ) {
|
||||||
|
let linkTitle = `${link.title}`
|
||||||
|
if ( link.tags.length ) {
|
||||||
|
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">${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>`)
|
||||||
|
}
|
||||||
|
|
||||||
|
mmLines.push('</ul></li>')
|
||||||
|
}
|
||||||
|
|
||||||
|
mmLines.push('</ul>')
|
||||||
|
mmLines.push('</section>')
|
||||||
|
}
|
||||||
|
|
||||||
|
return mmLines.join('\n')
|
||||||
|
}
|
||||||
|
}
|
40
src/markmark/markmark.renderer.ts
Normal file
40
src/markmark/markmark.renderer.ts
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import {isNamedSection, MarkMark} from './types'
|
||||||
|
|
||||||
|
export class MarkMarkRenderer {
|
||||||
|
public render(mm: MarkMark): string {
|
||||||
|
let mmLines: string[] = ['\n']
|
||||||
|
|
||||||
|
// Write the 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})`)
|
||||||
|
|
||||||
|
for ( const section of mm.sections ) {
|
||||||
|
mmLines.push('\n')
|
||||||
|
|
||||||
|
// if this section has a title/description, write those out
|
||||||
|
if ( isNamedSection(section) ) {
|
||||||
|
mmLines.push(`# ${section.title}\n`)
|
||||||
|
if ( section.description ) {
|
||||||
|
mmLines.push(`${section.description}\n`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for ( const link of section.links ) {
|
||||||
|
let linkTitle = `- ${link.title}`
|
||||||
|
if ( link.tags.length ) {
|
||||||
|
linkTitle += ` ${link.tags.map(x => '#' + x).join(' ')}`
|
||||||
|
}
|
||||||
|
|
||||||
|
mmLines.push(linkTitle)
|
||||||
|
|
||||||
|
for ( const url of link.urls ) {
|
||||||
|
mmLines.push(` - ${url}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return mmLines.join('\n')
|
||||||
|
}
|
||||||
|
}
|
136
src/markmark/parser.ts
Normal file
136
src/markmark/parser.ts
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
import * as marked from 'marked'
|
||||||
|
import {FrontMatter, isNamedSection, Link, MarkMark, Section} from './types'
|
||||||
|
|
||||||
|
export class Parser {
|
||||||
|
public parse(content: string): MarkMark {
|
||||||
|
const mm: MarkMark = {
|
||||||
|
frontmatter: {
|
||||||
|
syntax: 'v1',
|
||||||
|
},
|
||||||
|
sections: [],
|
||||||
|
}
|
||||||
|
|
||||||
|
let foundFrontmatter: boolean = false
|
||||||
|
let currentSection: Section = { links: [] }
|
||||||
|
let currentLink: Link|undefined
|
||||||
|
let sectionListItemsRemaining: number = 0
|
||||||
|
let linkListItemsRemaining: number = 0
|
||||||
|
const walkTokens = (token: marked.marked.Token) => {
|
||||||
|
// Parse out the front-matter
|
||||||
|
if ( token.type === 'paragraph' && !foundFrontmatter && token.raw.trim().startsWith('[//]:') ) {
|
||||||
|
mm.frontmatter = this.parseFrontmatter(token.raw.trim())
|
||||||
|
foundFrontmatter = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// When we encounter a heading, start a new section
|
||||||
|
if ( token.type === 'heading' ) {
|
||||||
|
if ( currentSection.links.length ) mm.sections.push(currentSection)
|
||||||
|
currentSection = {
|
||||||
|
title: token.text,
|
||||||
|
links: []
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// When we encounter a non-frontmatter paragraph and we're in a section,
|
||||||
|
// assume it's the description for the section
|
||||||
|
if ( token.type === 'paragraph' && isNamedSection(currentSection) && !token.raw.trim().startsWith('[//]:') ) {
|
||||||
|
currentSection.description = token.raw
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// If we're not currently parsing a section and we encounter a list,
|
||||||
|
// start parsing that list (grab the # of items in the list)
|
||||||
|
if ( !sectionListItemsRemaining && token.type === 'list' ) {
|
||||||
|
token.items.map(listItem => {
|
||||||
|
listItem.tokens.map(token => {
|
||||||
|
// Explicitly mark the top-level text/list tokens as "section" items
|
||||||
|
// to prevent double-counting. This is because `marked` parses text
|
||||||
|
// <li>'s as a text-w/in-a-text.
|
||||||
|
(token as any).mmIsSectionLevel = true
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
sectionListItemsRemaining = token.items.length + 1
|
||||||
|
return // to avoid conflict with linkListItemsRemaining
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we're parsing a section list and we're NOT parsing a link's URL list
|
||||||
|
// and we encounter some text, assume it's the name of a link and start parsing it
|
||||||
|
if ( sectionListItemsRemaining && !linkListItemsRemaining && token.type === 'text' && (token as any).mmIsSectionLevel ) {
|
||||||
|
currentLink = {
|
||||||
|
title: token.text.split(' #')[0].trim(),
|
||||||
|
tags: this.parseTags(token.text),
|
||||||
|
urls: [],
|
||||||
|
}
|
||||||
|
|
||||||
|
sectionListItemsRemaining -= 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// If we're parsing a section list but not a link URL list and we encounter a list,
|
||||||
|
// assume it's the inner list of link URLs and start parsing it
|
||||||
|
if ( sectionListItemsRemaining && !linkListItemsRemaining && token.type === 'list' ) {
|
||||||
|
linkListItemsRemaining = token.items.length + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we're parsing the URL list for a link and we encounter a link,
|
||||||
|
// add its URL to the URLs for currentLink
|
||||||
|
if ( currentLink && sectionListItemsRemaining && linkListItemsRemaining && token.type === 'link' ) {
|
||||||
|
currentLink.urls.push(token.href)
|
||||||
|
linkListItemsRemaining -= 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we were parsing a link and we ran out of URLs for the link,
|
||||||
|
// stop parsing that link and push it into the section
|
||||||
|
if ( currentLink && linkListItemsRemaining === 1 ) {
|
||||||
|
linkListItemsRemaining = 0
|
||||||
|
currentSection.links.push(currentLink)
|
||||||
|
currentLink = undefined
|
||||||
|
|
||||||
|
// If that was the last link in the section, end the section
|
||||||
|
if ( sectionListItemsRemaining === 1 ) {
|
||||||
|
mm.sections.push(currentSection)
|
||||||
|
sectionListItemsRemaining = 0
|
||||||
|
currentSection = { links: [] }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
marked.marked.use({ walkTokens })
|
||||||
|
marked.marked.parse(content)
|
||||||
|
|
||||||
|
mm.sections.push(currentSection)
|
||||||
|
mm.sections = mm.sections.filter(s => s.links.length)
|
||||||
|
|
||||||
|
return mm
|
||||||
|
}
|
||||||
|
|
||||||
|
protected parseFrontmatter(text: string): FrontMatter {
|
||||||
|
const fm: FrontMatter = {
|
||||||
|
syntax: 'v1',
|
||||||
|
}
|
||||||
|
|
||||||
|
const matcher = /\[\/\/]:\s+#\(([a-zA-Z0-9_\-]+):\s+(.*)\)/g
|
||||||
|
const rawFrontmatter: Record<string, string> =
|
||||||
|
[...text.matchAll(matcher)]
|
||||||
|
.map(match => ({[match[1]]: match[2]}))
|
||||||
|
.reduce((carry, current) => ({...carry, ...current}), {})
|
||||||
|
|
||||||
|
if ( rawFrontmatter['markmark-author-name'] ) fm.authorName = rawFrontmatter['markmark-author-name']
|
||||||
|
if ( rawFrontmatter['markmark-author-email'] ) fm.authorEmail = rawFrontmatter['markmark-author-email']
|
||||||
|
if ( rawFrontmatter['markmark-author-href'] ) fm.authorHref = rawFrontmatter['markmark-author-href']
|
||||||
|
|
||||||
|
return fm
|
||||||
|
}
|
||||||
|
|
||||||
|
protected parseTags(text: string): string[] {
|
||||||
|
const matcher = /#([a-zA-Z0-9_\-]+)/g
|
||||||
|
return [...text.matchAll(matcher)].map(x => x[1])
|
||||||
|
}
|
||||||
|
}
|
26
src/markmark/types.ts
Normal file
26
src/markmark/types.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import {hasOwnProperty} from '@extollo/lib'
|
||||||
|
|
||||||
|
export type MarkMark = {
|
||||||
|
frontmatter: FrontMatter,
|
||||||
|
sections: Section[],
|
||||||
|
}
|
||||||
|
|
||||||
|
export type FrontMatter = {
|
||||||
|
syntax: 'v1',
|
||||||
|
authorName?: string,
|
||||||
|
authorEmail?: string,
|
||||||
|
authorHref?: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AnonymousSection = { links: Link[] }
|
||||||
|
export type NamedSection = { title: string, description?: string, links: Link[] }
|
||||||
|
export type Section = AnonymousSection | NamedSection
|
||||||
|
|
||||||
|
export const isNamedSection = (what: Section): what is NamedSection =>
|
||||||
|
hasOwnProperty(what, 'title') && (typeof what.title === 'string')
|
||||||
|
|
||||||
|
export type Link = {
|
||||||
|
title: string,
|
||||||
|
tags: string[],
|
||||||
|
urls: string[],
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user