Skip to content

Commit

Permalink
Add JSDoc based types
Browse files Browse the repository at this point in the history
  • Loading branch information
wooorm committed Aug 6, 2021
1 parent 569f6e2 commit 650b75e
Show file tree
Hide file tree
Showing 9 changed files with 185 additions and 121 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
coverage/
node_modules/
.DS_Store
*.d.ts
*.log
yarn.lock
203 changes: 138 additions & 65 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,28 @@
/**
* @typedef {import('mdast').Root} Root
*
* @typedef Options
* Configuration.
* @property {boolean} [mentionStrong=true]
* Wrap mentions in `<strong>`, true by default.
* This makes them render more like how GitHub styles them.
* But GitHub itself uses CSS instead of a strong.
* @property {string} [repository]
* Repository to link against.
*/

import fs from 'fs'
import path from 'path'
import {visit} from 'unist-util-visit'
import {toString} from 'mdast-util-to-string'
import {findAndReplace} from 'mdast-util-find-and-replace'

// Hide process use from browserify and the like.
const proc = typeof global !== 'undefined' && global.process
const proc =
typeof global === 'undefined'
? /* c8 ignore next */
{cwd: () => '/'}
: global.process

// Previously, GitHub linked `@mention` and `@mentions` to their blog post about
// mentions (<https://github.com/blog/821>).
Expand Down Expand Up @@ -72,33 +89,51 @@ const mentionRegex = new RegExp(
'gi'
)

export default function remarkGithub(options) {
const settings = options || {}
let repository = settings.repository
/**
* Plugin to enable, disable, and ignore messages.
*
* @type {import('unified').Plugin<[Options?]|void[], Root>}
*/
export default function remarkGithub(options = {}) {
/**
* @typedef {import('mdast').StaticPhrasingContent} StaticPhrasingContent
* @typedef {import('mdast-util-find-and-replace').ReplaceFunction} ReplaceFunction
* @typedef {import('type-fest').PackageJson} PackageJson
* @typedef {{input: string, index: number}} Match
*/

let repository = options.repository
/** @type {PackageJson|undefined} */
let pkg

// Get the repository from `package.json`.
if (!repository) {
try {
pkg = JSON.parse(fs.readFileSync(path.join(proc.cwd(), 'package.json')))
pkg = JSON.parse(
String(fs.readFileSync(path.join(proc.cwd(), 'package.json')))
)
} catch {}

repository =
pkg && pkg.repository ? pkg.repository.url || pkg.repository : ''
pkg && pkg.repository
? // Object form.
/* c8 ignore next 2 */
typeof pkg.repository === 'object'
? pkg.repository.url
: pkg.repository
: ''
}

// Parse the URL: See the tests for all possible kinds.
repository = repoRegex.exec(repository)
const repositoryMatch = repoRegex.exec(repository || '')

if (!repository) {
if (!repositoryMatch) {
throw new Error('Missing `repository` field in `options`')
}

repository = {user: repository[1], project: repository[2]}
const repositoryInfo = {user: repositoryMatch[1], project: repositoryMatch[2]}

return transformer

function transformer(tree) {
return (tree) => {
findAndReplace(
tree,
[
Expand All @@ -109,12 +144,57 @@ export default function remarkGithub(options) {
],
{ignore: ['link', 'linkReference']}
)
visit(tree, 'link', visitor)
visit(tree, 'link', (node) => {
const link = parse(node)

if (!link) {
return
}

const comment = link.comment ? ' (comment)' : ''
/** @type {string} */
let base

if (link.project !== repositoryInfo.project) {
base = link.user + '/' + link.project
} else if (link.user === repositoryInfo.user) {
base = ''
} else {
base = link.user
}

/** @type {StaticPhrasingContent[]} */
const children = []

if (link.page === 'commit') {
if (base) {
children.push({type: 'text', value: base + '@'})
}

children.push({type: 'inlineCode', value: abbr(link.reference)})

if (link.comment) {
children.push({type: 'text', value: comment})
}
} else {
base += '#'
children.push({
type: 'text',
value: base + abbr(link.reference) + comment
})
}

node.children = children
})
}

/**
* @type {ReplaceFunction}
* @param {string} value
* @param {string} username
* @param {Match} match
*/
function replaceMention(value, username, match) {
let node

if (
/[\w`]/.test(match.input.charAt(match.index - 1)) ||
/[/\w`]/.test(match.input.charAt(match.index + value.length)) ||
Expand All @@ -123,9 +203,10 @@ export default function remarkGithub(options) {
return false
}

node = {type: 'text', value}
/** @type {StaticPhrasingContent} */
let node = {type: 'text', value}

if (settings.mentionStrong !== false) {
if (options.mentionStrong !== false) {
node = {type: 'strong', children: [node]}
}

Expand All @@ -137,6 +218,12 @@ export default function remarkGithub(options) {
}
}

/**
* @type {ReplaceFunction}
* @param {string} value
* @param {string} no
* @param {Match} match
*/
function replaceIssue(value, no, match) {
if (
/\w/.test(match.input.charAt(match.index - 1)) ||
Expand All @@ -150,15 +237,20 @@ export default function remarkGithub(options) {
title: null,
url:
'https://github.com/' +
repository.user +
repositoryInfo.user +
'/' +
repository.project +
repositoryInfo.project +
'/issues/' +
no,
children: [{type: 'text', value}]
}
}

/**
* @type {ReplaceFunction}
* @param {string} value
* @param {Match} match
*/
function replaceHash(value, match) {
if (
/[^\t\n\r (@[{]/.test(match.input.charAt(match.index - 1)) ||
Expand All @@ -173,15 +265,24 @@ export default function remarkGithub(options) {
title: null,
url:
'https://github.com/' +
repository.user +
repositoryInfo.user +
'/' +
repository.project +
repositoryInfo.project +
'/commit/' +
value,
children: [{type: 'inlineCode', value: abbr(value)}]
}
}

/**
* @type {ReplaceFunction}
* @param {string} $0
* @param {string} user
* @param {string} project
* @param {string} no
* @param {string} sha
* @param {Match} match
*/
// eslint-disable-next-line max-params
function replaceReference($0, user, project, no, sha, match) {
let value = ''
Expand All @@ -193,13 +294,14 @@ export default function remarkGithub(options) {
return false
}

/** @type {StaticPhrasingContent[]} */
const nodes = []

if (user !== repository.user) {
if (user !== repositoryInfo.user) {
value += user
}

if (project && project !== repository.project) {
if (project && project !== repositoryInfo.project) {
value = user + '/' + project
}

Expand All @@ -219,61 +321,32 @@ export default function remarkGithub(options) {
'https://github.com/' +
user +
'/' +
(project || repository.project) +
(project || repositoryInfo.project) +
'/' +
(no ? 'issues' : 'commit') +
'/' +
(no || sha),
children: nodes
}
}

function visitor(node) {
const link = parse(node)
let children
let base

if (!link) {
return
}

const comment = link.comment ? ' (comment)' : ''

if (link.project !== repository.project) {
base = link.user + '/' + link.project
} else if (link.user === repository.user) {
base = ''
} else {
base = link.user
}

if (link.page === 'commit') {
children = []

if (base) {
children.push({type: 'text', value: base + '@'})
}

children.push({type: 'inlineCode', value: abbr(link.reference)})

if (link.comment) {
children.push({type: 'text', value: comment})
}
} else {
base += '#'
children = [{type: 'text', value: base + abbr(link.reference) + comment}]
}

node.children = children
}
}

// Abbreviate a SHA.
/**
* Abbreviate a SHA.
*
* @param {string} sha
* @returns {string}
*/
function abbr(sha) {
return sha.slice(0, minShaLength)
}

// Parse a link and determine whether it links to GitHub.
/**
* Parse a link and determine whether it links to GitHub.
*
* @param {import('mdast').Link} node
* @returns {{user: string, project: string, page: string, reference: string, comment: boolean}|undefined}
*/
function parse(node) {
const url = node.url || ''
const match = linkRegex.exec(url)
Expand Down
25 changes: 18 additions & 7 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,31 +35,39 @@
"sideEffects": false,
"type": "module",
"main": "index.js",
"types": "index.d.ts",
"files": [
"index.d.ts",
"index.js"
],
"dependencies": {
"@types/mdast": "^3.0.0",
"mdast-util-find-and-replace": "^2.0.0",
"mdast-util-to-string": "^3.0.0",
"unified": "^10.0.0",
"unist-util-visit": "^4.0.0"
},
"devDependencies": {
"@types/tape": "^4.0.0",
"c8": "^7.0.0",
"prettier": "^2.0.0",
"remark": "^14.0.0",
"remark-cli": "^10.0.0",
"remark-gfm": "^2.0.0",
"remark-preset-wooorm": "^8.0.0",
"rimraf": "^3.0.0",
"tape": "^5.0.0",
"unified": "^10.0.0",
"type-coverage": "^2.0.0",
"type-fest": "^2.0.0",
"typescript": "^4.0.0",
"xo": "^0.39.0"
},
"scripts": {
"build": "rimraf \"test/**/*.d.ts\" \"*.d.ts\" && tsc && type-coverage",
"format": "remark . -qfo --ignore-pattern test/ && prettier . -w --loglevel warn && xo --fix",
"test-api": "node --conditions development test/index.js",
"test-coverage": "c8 --check-coverage --branches 100 --functions 100 --lines 100 --statements 100 --reporter lcov npm run test-api",
"test-types": "dtslint types",
"test": "npm run format && npm run test-coverage && npm run test-types"
"test": "npm run build && npm run format && npm run test-coverage"
},
"prettier": {
"tabWidth": 2,
Expand All @@ -70,14 +78,17 @@
"trailingComma": "none"
},
"xo": {
"prettier": true,
"ignores": [
"types/"
]
"prettier": true
},
"remarkConfig": {
"plugins": [
"preset-wooorm"
]
},
"typeCoverage": {
"atLeast": 100,
"detail": true,
"strict": true,
"ignoreCatch": true
}
}
Loading

0 comments on commit 650b75e

Please sign in to comment.