Big bang
This commit is contained in:
commit
e243350f95
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
lib
|
||||
node_modules
|
||||
.idea
|
36
package.json
Normal file
36
package.json
Normal file
@ -0,0 +1,36 @@
|
||||
{
|
||||
"name": "mark-mark",
|
||||
"version": "0.1.0",
|
||||
"description": "A TypeScript MarkMark parser/renderer collection",
|
||||
"homepage": "https://garrettmills.dev/markmark",
|
||||
"main": "lib/index.js",
|
||||
"types": "lib/index.d.ts",
|
||||
"directories": {
|
||||
"lib": "lib"
|
||||
},
|
||||
"files": [
|
||||
"lib/**/*"
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://code.garrettmills.dev/garrettmills/mark-mark"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "rimraf lib && tsc"
|
||||
},
|
||||
"keywords": [
|
||||
"markmark",
|
||||
"bookmarks"
|
||||
],
|
||||
"author": "Garrett Mills <shout@garrettmills.dev>",
|
||||
"license": "AGPL-3.0-only",
|
||||
"packageManager": "pnpm@10.6.5",
|
||||
"devDependencies": {
|
||||
"rimraf": "^6.0.1",
|
||||
"typescript": "^5.8.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"hasha": "^6.0.0",
|
||||
"marked": "^15.0.12"
|
||||
}
|
||||
}
|
328
pnpm-lock.yaml
Normal file
328
pnpm-lock.yaml
Normal file
@ -0,0 +1,328 @@
|
||||
lockfileVersion: '9.0'
|
||||
|
||||
settings:
|
||||
autoInstallPeers: true
|
||||
excludeLinksFromLockfile: false
|
||||
|
||||
importers:
|
||||
|
||||
.:
|
||||
dependencies:
|
||||
hasha:
|
||||
specifier: ^6.0.0
|
||||
version: 6.0.0
|
||||
marked:
|
||||
specifier: ^15.0.12
|
||||
version: 15.0.12
|
||||
devDependencies:
|
||||
rimraf:
|
||||
specifier: ^6.0.1
|
||||
version: 6.0.1
|
||||
typescript:
|
||||
specifier: ^5.8.3
|
||||
version: 5.8.3
|
||||
|
||||
packages:
|
||||
|
||||
'@isaacs/cliui@8.0.2':
|
||||
resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
ansi-regex@5.0.1:
|
||||
resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
ansi-regex@6.1.0:
|
||||
resolution: {integrity: sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
ansi-styles@4.3.0:
|
||||
resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
ansi-styles@6.2.1:
|
||||
resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
balanced-match@1.0.2:
|
||||
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
|
||||
|
||||
brace-expansion@2.0.1:
|
||||
resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==}
|
||||
|
||||
color-convert@2.0.1:
|
||||
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
|
||||
engines: {node: '>=7.0.0'}
|
||||
|
||||
color-name@1.1.4:
|
||||
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
|
||||
|
||||
cross-spawn@7.0.6:
|
||||
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
|
||||
engines: {node: '>= 8'}
|
||||
|
||||
eastasianwidth@0.2.0:
|
||||
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
|
||||
|
||||
emoji-regex@8.0.0:
|
||||
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
|
||||
|
||||
emoji-regex@9.2.2:
|
||||
resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==}
|
||||
|
||||
foreground-child@3.3.1:
|
||||
resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==}
|
||||
engines: {node: '>=14'}
|
||||
|
||||
glob@11.0.2:
|
||||
resolution: {integrity: sha512-YT7U7Vye+t5fZ/QMkBFrTJ7ZQxInIUjwyAjVj84CYXqgBdv30MFUPGnBR6sQaVq6Is15wYJUsnzTuWaGRBhBAQ==}
|
||||
engines: {node: 20 || >=22}
|
||||
hasBin: true
|
||||
|
||||
hasha@6.0.0:
|
||||
resolution: {integrity: sha512-MLydoyGp9QJcjlhE5lsLHXYpWayjjWqkavzju2ZWD2tYa1CgmML1K1gWAu22BLFa2eZ0OfvJ/DlfoVjaD54U2Q==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
is-fullwidth-code-point@3.0.0:
|
||||
resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
is-stream@3.0.0:
|
||||
resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==}
|
||||
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
||||
|
||||
isexe@2.0.0:
|
||||
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
|
||||
|
||||
jackspeak@4.1.1:
|
||||
resolution: {integrity: sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==}
|
||||
engines: {node: 20 || >=22}
|
||||
|
||||
lru-cache@11.1.0:
|
||||
resolution: {integrity: sha512-QIXZUBJUx+2zHUdQujWejBkcD9+cs94tLn0+YL8UrCh+D5sCXZ4c7LaEH48pNwRY3MLDgqUFyhlCyjJPf1WP0A==}
|
||||
engines: {node: 20 || >=22}
|
||||
|
||||
marked@15.0.12:
|
||||
resolution: {integrity: sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==}
|
||||
engines: {node: '>= 18'}
|
||||
hasBin: true
|
||||
|
||||
minimatch@10.0.1:
|
||||
resolution: {integrity: sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==}
|
||||
engines: {node: 20 || >=22}
|
||||
|
||||
minipass@7.1.2:
|
||||
resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==}
|
||||
engines: {node: '>=16 || 14 >=14.17'}
|
||||
|
||||
package-json-from-dist@1.0.1:
|
||||
resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==}
|
||||
|
||||
path-key@3.1.1:
|
||||
resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
path-scurry@2.0.0:
|
||||
resolution: {integrity: sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==}
|
||||
engines: {node: 20 || >=22}
|
||||
|
||||
rimraf@6.0.1:
|
||||
resolution: {integrity: sha512-9dkvaxAsk/xNXSJzMgFqqMCuFgt2+KsOFek3TMLfo8NCPfWpBmqwyNn5Y+NX56QUYfCtsyhF3ayiboEoUmJk/A==}
|
||||
engines: {node: 20 || >=22}
|
||||
hasBin: true
|
||||
|
||||
shebang-command@2.0.0:
|
||||
resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
shebang-regex@3.0.0:
|
||||
resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
signal-exit@4.1.0:
|
||||
resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
|
||||
engines: {node: '>=14'}
|
||||
|
||||
string-width@4.2.3:
|
||||
resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
string-width@5.1.2:
|
||||
resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
strip-ansi@6.0.1:
|
||||
resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
strip-ansi@7.1.0:
|
||||
resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
type-fest@4.41.0:
|
||||
resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==}
|
||||
engines: {node: '>=16'}
|
||||
|
||||
typescript@5.8.3:
|
||||
resolution: {integrity: sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==}
|
||||
engines: {node: '>=14.17'}
|
||||
hasBin: true
|
||||
|
||||
which@2.0.2:
|
||||
resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
|
||||
engines: {node: '>= 8'}
|
||||
hasBin: true
|
||||
|
||||
wrap-ansi@7.0.0:
|
||||
resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
wrap-ansi@8.1.0:
|
||||
resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
snapshots:
|
||||
|
||||
'@isaacs/cliui@8.0.2':
|
||||
dependencies:
|
||||
string-width: 5.1.2
|
||||
string-width-cjs: string-width@4.2.3
|
||||
strip-ansi: 7.1.0
|
||||
strip-ansi-cjs: strip-ansi@6.0.1
|
||||
wrap-ansi: 8.1.0
|
||||
wrap-ansi-cjs: wrap-ansi@7.0.0
|
||||
|
||||
ansi-regex@5.0.1: {}
|
||||
|
||||
ansi-regex@6.1.0: {}
|
||||
|
||||
ansi-styles@4.3.0:
|
||||
dependencies:
|
||||
color-convert: 2.0.1
|
||||
|
||||
ansi-styles@6.2.1: {}
|
||||
|
||||
balanced-match@1.0.2: {}
|
||||
|
||||
brace-expansion@2.0.1:
|
||||
dependencies:
|
||||
balanced-match: 1.0.2
|
||||
|
||||
color-convert@2.0.1:
|
||||
dependencies:
|
||||
color-name: 1.1.4
|
||||
|
||||
color-name@1.1.4: {}
|
||||
|
||||
cross-spawn@7.0.6:
|
||||
dependencies:
|
||||
path-key: 3.1.1
|
||||
shebang-command: 2.0.0
|
||||
which: 2.0.2
|
||||
|
||||
eastasianwidth@0.2.0: {}
|
||||
|
||||
emoji-regex@8.0.0: {}
|
||||
|
||||
emoji-regex@9.2.2: {}
|
||||
|
||||
foreground-child@3.3.1:
|
||||
dependencies:
|
||||
cross-spawn: 7.0.6
|
||||
signal-exit: 4.1.0
|
||||
|
||||
glob@11.0.2:
|
||||
dependencies:
|
||||
foreground-child: 3.3.1
|
||||
jackspeak: 4.1.1
|
||||
minimatch: 10.0.1
|
||||
minipass: 7.1.2
|
||||
package-json-from-dist: 1.0.1
|
||||
path-scurry: 2.0.0
|
||||
|
||||
hasha@6.0.0:
|
||||
dependencies:
|
||||
is-stream: 3.0.0
|
||||
type-fest: 4.41.0
|
||||
|
||||
is-fullwidth-code-point@3.0.0: {}
|
||||
|
||||
is-stream@3.0.0: {}
|
||||
|
||||
isexe@2.0.0: {}
|
||||
|
||||
jackspeak@4.1.1:
|
||||
dependencies:
|
||||
'@isaacs/cliui': 8.0.2
|
||||
|
||||
lru-cache@11.1.0: {}
|
||||
|
||||
marked@15.0.12: {}
|
||||
|
||||
minimatch@10.0.1:
|
||||
dependencies:
|
||||
brace-expansion: 2.0.1
|
||||
|
||||
minipass@7.1.2: {}
|
||||
|
||||
package-json-from-dist@1.0.1: {}
|
||||
|
||||
path-key@3.1.1: {}
|
||||
|
||||
path-scurry@2.0.0:
|
||||
dependencies:
|
||||
lru-cache: 11.1.0
|
||||
minipass: 7.1.2
|
||||
|
||||
rimraf@6.0.1:
|
||||
dependencies:
|
||||
glob: 11.0.2
|
||||
package-json-from-dist: 1.0.1
|
||||
|
||||
shebang-command@2.0.0:
|
||||
dependencies:
|
||||
shebang-regex: 3.0.0
|
||||
|
||||
shebang-regex@3.0.0: {}
|
||||
|
||||
signal-exit@4.1.0: {}
|
||||
|
||||
string-width@4.2.3:
|
||||
dependencies:
|
||||
emoji-regex: 8.0.0
|
||||
is-fullwidth-code-point: 3.0.0
|
||||
strip-ansi: 6.0.1
|
||||
|
||||
string-width@5.1.2:
|
||||
dependencies:
|
||||
eastasianwidth: 0.2.0
|
||||
emoji-regex: 9.2.2
|
||||
strip-ansi: 7.1.0
|
||||
|
||||
strip-ansi@6.0.1:
|
||||
dependencies:
|
||||
ansi-regex: 5.0.1
|
||||
|
||||
strip-ansi@7.1.0:
|
||||
dependencies:
|
||||
ansi-regex: 6.1.0
|
||||
|
||||
type-fest@4.41.0: {}
|
||||
|
||||
typescript@5.8.3: {}
|
||||
|
||||
which@2.0.2:
|
||||
dependencies:
|
||||
isexe: 2.0.0
|
||||
|
||||
wrap-ansi@7.0.0:
|
||||
dependencies:
|
||||
ansi-styles: 4.3.0
|
||||
string-width: 4.2.3
|
||||
strip-ansi: 6.0.1
|
||||
|
||||
wrap-ansi@8.1.0:
|
||||
dependencies:
|
||||
ansi-styles: 6.2.1
|
||||
string-width: 5.1.2
|
||||
strip-ansi: 7.1.0
|
3
src/index.ts
Normal file
3
src/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export * from './types'
|
||||
export * from './parser'
|
||||
export * from './renderers'
|
157
src/parser.ts
Normal file
157
src/parser.ts
Normal file
@ -0,0 +1,157 @@
|
||||
import * as marked from 'marked'
|
||||
import {FrontMatter, isNamedSection, Link, MarkMark, Section} from './types'
|
||||
import * as hasha from 'hasha'
|
||||
|
||||
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.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: marked.Tokens.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 ) {
|
||||
const [title, date] = this.parseTitleAndDate(token.text.split(' #')[0].trim())
|
||||
currentLink = {
|
||||
title,
|
||||
date,
|
||||
hash: hasha.hashSync(token.text, { algorithm: 'sha256' }),
|
||||
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 parseTitleAndDate(text: string): [string, Date|undefined] {
|
||||
text = text.trim()
|
||||
|
||||
const dateMatcher = /(.*)\(([0-9\-+:TZ]+)\)$/g
|
||||
const result = dateMatcher.exec(text)
|
||||
if ( !result ) {
|
||||
return [text, undefined]
|
||||
}
|
||||
|
||||
const [, title, dateString] = result
|
||||
const date = new Date(dateString)
|
||||
if ( isNaN(date.getTime()) ) {
|
||||
return [text, undefined]
|
||||
}
|
||||
|
||||
return [title.trim(), date]
|
||||
}
|
||||
|
||||
protected parseTags(text: string): string[] {
|
||||
const matcher = /#([a-zA-Z0-9_\-]+)/g
|
||||
return [...text.matchAll(matcher)].map(x => x[1])
|
||||
}
|
||||
}
|
54
src/renderers/html.ts
Normal file
54
src/renderers/html.ts
Normal file
@ -0,0 +1,54 @@
|
||||
import * as marked from 'marked'
|
||||
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">${marked.marked.parse(section.title)}</h1>`)
|
||||
if ( section.description ) {
|
||||
mmLines.push(`<p class="markmark section-description">${marked.marked.parse(section.description)}</p>`)
|
||||
}
|
||||
}
|
||||
|
||||
mmLines.push('<ul class="markmark section-list">')
|
||||
|
||||
for ( const link of section.links ) {
|
||||
let linkTitle = `${link.title}`
|
||||
|
||||
if ( link.date ) {
|
||||
linkTitle += ` <span class="markmark link-date">(${this.formatDate(link.date)})</span>`
|
||||
}
|
||||
|
||||
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" id="link-${link.hash}">${marked.marked.parse(linkTitle)}<ul class="markmark url-list">`)
|
||||
|
||||
for ( const url of link.urls ) {
|
||||
mmLines.push(`<li class="markmark link-url"><a href="${url}" target="_blank">${url}</a></li>`)
|
||||
}
|
||||
|
||||
mmLines.push('</ul></li>')
|
||||
}
|
||||
|
||||
mmLines.push('</ul>')
|
||||
mmLines.push('</section>')
|
||||
}
|
||||
|
||||
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}`;
|
||||
}
|
||||
}
|
2
src/renderers/index.ts
Normal file
2
src/renderers/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './html'
|
||||
export * from './markmark'
|
54
src/renderers/markmark.ts
Normal file
54
src/renderers/markmark.ts
Normal file
@ -0,0 +1,54 @@
|
||||
import {isNamedSection, MarkMark} from '../types'
|
||||
|
||||
export class MarkMarkRenderer {
|
||||
public render(mm: MarkMark, frontmatter: boolean = true): string {
|
||||
let mmLines: string[] = ['\n']
|
||||
|
||||
// Write the frontmatter
|
||||
if ( frontmatter ) {
|
||||
mmLines.push(`[//]: #(markmark-syntax: ${mm.frontmatter.syntax})`)
|
||||
if (mm.frontmatter.authorName) mmLines.push(`[//]: #(markmark-author-name: ${mm.frontmatter.authorName})`)
|
||||
if (mm.frontmatter.authorEmail) mmLines.push(`[//]: #(markmark-author-email: ${mm.frontmatter.authorEmail})`)
|
||||
if (mm.frontmatter.authorHref) mmLines.push(`[//]: #(markmark-author-href: ${mm.frontmatter.authorHref})`)
|
||||
}
|
||||
|
||||
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.date ) {
|
||||
linkTitle += ` (${this.formatDate(link.date)})`
|
||||
}
|
||||
|
||||
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')
|
||||
}
|
||||
|
||||
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}`;
|
||||
}
|
||||
}
|
31
src/types.ts
Normal file
31
src/types.ts
Normal file
@ -0,0 +1,31 @@
|
||||
/** A typescript-compatible version of Object.hasOwnProperty. Yoinked from @extollo/lib. */
|
||||
export function hasOwnProperty<X extends {}, Y extends PropertyKey>(obj: X, prop: Y): obj is X & Record<Y, unknown> {
|
||||
return Object.hasOwnProperty.call(obj, prop)
|
||||
}
|
||||
|
||||
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 = {
|
||||
hash: string,
|
||||
title: string,
|
||||
date?: Date,
|
||||
tags: string[],
|
||||
urls: string[],
|
||||
}
|
16
tsconfig.json
Normal file
16
tsconfig.json
Normal file
@ -0,0 +1,16 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es6",
|
||||
"module": "commonjs",
|
||||
"declaration": true,
|
||||
"outDir": "./lib",
|
||||
"strict": true,
|
||||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"skipLibCheck": true,
|
||||
"lib": ["ESNext"]
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user