Add initial MarkMark spec and integrate my own links.mark.md file
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
Garrett Mills 2023-11-20 22:53:59 -06:00
parent a6e1819d2d
commit 575a253651
19 changed files with 871 additions and 5 deletions

View File

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

View File

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

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

View File

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

View File

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

View 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

View 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

View 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

View 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).

View 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); }

View 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();

View 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();

View File

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

View File

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

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

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

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