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 4, 2021
1 parent 3c4b48d commit 4a12931
Show file tree
Hide file tree
Showing 7 changed files with 193 additions and 48 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
7 changes: 7 additions & 0 deletions formatters.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
/**
* @typedef {import('./index.js').FormatterObjects} FormatterObjects
* @typedef {import('./index.js').Format} Format
*/

import {u} from 'unist-builder'

/** @type {FormatterObjects} */
export const defaultFormatters = {
email: {exclude: true},
name: {
Expand All @@ -13,6 +19,7 @@ export const defaultFormatters = {
twitter: {label: 'Twitter', format: profile}
}

/** @type {Format} */
function profile(value, key) {
let pos = value.toLowerCase().indexOf('.com/')

Expand Down
174 changes: 135 additions & 39 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,68 @@
/**
* @typedef {import('mdast').Root} Root
* @typedef {import('mdast').PhrasingContent} PhrasingContent
* @typedef {import('mdast').BlockContent} BlockContent
* @typedef {import('mdast').TableContent} TableContent
* @typedef {import('mdast').RowContent} RowContent
* @typedef {import('mdast').AlignType} AlignType
* @typedef {import('vfile').VFile} VFile
* @typedef {Record<string, unknown>} ContributorObject
* @typedef {ContributorObject|string} Contributor
* @typedef {Record<string, FormatterObject>} FormatterObjects
*
* @callback Format
* @param {string} value
* The value of a field in a contributor.
* @param {string} key
* The name of a field in a contributor.
* @param {Contributor} contributor
* The whole contributor.
* @returns {null|undefined|string|PhrasingContent|PhrasingContent[]}
*
* @typedef FormatterObject
* @property {string} [label]
* Text in the header row that labels the column for this field.
* @property {boolean} [exclude=false]
* Whether to ignore these fields.
* @property {Format} [format]
* How to format the cell.
*
* @typedef {string|boolean|null|undefined|FormatterObject} Formatter
*
* @typedef {Record<string, Formatter>} Formatters
*
* @typedef Options
* Configuration.
* @property {Contributor[]} [contributors]
* List of contributors to inject.
* Defaults to the `contributors` field in the closest `package.json` upwards
* from the processed file, if there is one.
* Supports the string form (`name <email> (url)`) as well.
* Fails if no contributors are found or given.
* @property {AlignType} [align]
* Alignment to use for all cells in the table.
* @property {boolean} [appendIfMissing=false]
* Inject the section if there is none.
* @property {string|RegExp} [heading='contributors']
* Heading to look for.
* @property {Formatters} [formatters=[]]
* Map of fields found in `contributors` to formatters.
* These given formatters extend the default formatters.
*
* The keys in `formatters` should correspond directly (case-sensitive) to
* keys in `contributors`.
*
* The values can be:
*
* * `null` or `undefined` — does nothing.
* * `false` — shortcut for `{label: key, exclude: true}`, can be used to
* exclude default formatters.
* * `true` — shortcut for `{label: key}`, can be used to include default
* formatters (like `email`)
* * `string` — shortcut for `{label: value}`
* * `Formatter` — …or a proper formatter object
*/

import path from 'path'
import isUrl from 'is-url'
import {findUpOne} from 'vfile-find-up'
Expand All @@ -9,16 +74,20 @@ import {defaultFormatters} from './formatters.js'

const own = {}.hasOwnProperty

export default function remarkContributors(options) {
const settings = options || {}
const align = settings.align || null
const defaultContributors = settings.contributors
const formatters = createFormatters(settings.formatters)
const contributorsHeading = settings.heading || 'contributors'

return transform

async function transform(tree, file) {
/**
* Plugin to inject a given list of contributors into a table.
*
* @type {import('unified').Plugin<[Options?] | void[], Root>}
* @returns {(node: Root, file: VFile) => Promise<void>}
*/
export default function remarkContributors(options = {}) {
const align = options.align || null
const defaultContributors = options.contributors
const formatters = createFormatters(options.formatters)
const contributorsHeading = options.heading || 'contributors'

return async (tree, file) => {
/** @type {Contributor[]|undefined} */
let rawContributors

if (defaultContributors) {
Expand All @@ -32,20 +101,24 @@ export default function remarkContributors(options) {

if (packageFile) {
await read(packageFile)
rawContributors = JSON.parse(String(packageFile)).contributors
/** @type {import('type-fest').PackageJson} */
const pack = JSON.parse(String(packageFile))
rawContributors = pack.contributors
}
} else {
throw new Error(
'Missing required `path` on `file`.\nMake sure it’s defined or pass `contributors` to `remark-contributors`'
)
}

/** @type {ContributorObject[]} */
const contributors = []
let index = -1

if (rawContributors) {
while (++index < rawContributors.length) {
const value = rawContributors[index]
// @ts-expect-error: indexable.
contributors.push(typeof value === 'string' ? parse(value) : value)
}
}
Expand All @@ -59,45 +132,54 @@ export default function remarkContributors(options) {
const table = createTable(contributors, formatters, align)
let headingFound = false

headingRange(tree, contributorsHeading, onheading)

// Add the section if not found but with `appendIfMissing`.
if (!headingFound && settings.appendIfMissing) {
tree.children.push(
u('heading', {depth: 2}, [u('text', 'Contributors')]),
table
)
}

function onheading(start, nodes, end) {
headingRange(tree, contributorsHeading, (start, nodes, end) => {
let siblings = /** @type {BlockContent[]} */ (nodes)
let index = -1
let tableFound
let tableFound = false

headingFound = true

while (++index < nodes.length) {
const node = nodes[index]
while (++index < siblings.length) {
const node = siblings[index]

if (node.type === 'table') {
tableFound = true
nodes = nodes.slice(0, index).concat(table, nodes.slice(index + 1))
siblings = siblings
.slice(0, index)
.concat(table, siblings.slice(index + 1))
break
}
}

if (!tableFound) {
nodes = [table].concat(nodes)
siblings = [table, ...siblings]
}

return [start].concat(nodes, end)
return [start, ...siblings, end]
})

// Add the section if not found but with `appendIfMissing`.
if (!headingFound && options.appendIfMissing) {
tree.children.push(
{type: 'heading', depth: 2, children: [u('text', 'Contributors')]},
table
)
}
}
}

/**
* @param {ContributorObject[]} contributors
* @param {FormatterObjects} formatters
* @param {AlignType} align
*/
function createTable(contributors, formatters, align) {
const keys = createKeys(contributors, formatters)
/** @type {TableContent[]} */
const rows = []
/** @type {RowContent[]} */
const cells = []
/** @type {AlignType[]} */
const aligns = []
let rowIndex = -1
let cellIndex = -1
Expand All @@ -113,21 +195,23 @@ function createTable(contributors, formatters, align) {

while (++rowIndex < contributors.length) {
const contributor = contributors[rowIndex]
/** @type {RowContent[]} */
const cells = []
let cellIndex = -1

while (++cellIndex < keys.length) {
const key = keys[cellIndex]
const format = (formatters[key] && formatters[key].format) || ((d) => d)
let value = contributor[key]

if (value === null || value === undefined) {
value = ''
} else if (typeof value === 'number') {
value = String(value)
}

value = format(value, key, contributor)
const format =
(formatters[key] && formatters[key].format) ||
/** @type {Format} */ ((d) => d)

let value = format(
contributor[key] === null || contributor[key] === undefined
? ''
: String(contributor[key]),
key,
contributor
)

if (typeof value === 'string') {
value = isUrl(value)
Expand All @@ -137,7 +221,7 @@ function createTable(contributors, formatters, align) {

if (value === null || value === undefined) {
value = []
} else if (typeof value.length !== 'number') {
} else if (!Array.isArray(value)) {
value = [value]
}

Expand All @@ -150,12 +234,19 @@ function createTable(contributors, formatters, align) {
return u('table', {align: aligns}, rows)
}

/**
* @param {ContributorObject[]} contributors
* @param {FormatterObjects} formatters
* @returns {string[]}
*/
function createKeys(contributors, formatters) {
/** @type {string[]} */
const labels = []
let index = -1

while (++index < contributors.length) {
const contributor = contributors[index]
/** @type {string} */
let field

for (field in contributor) {
Expand All @@ -176,13 +267,18 @@ function createKeys(contributors, formatters) {
return labels
}

/**
* @param {Formatters|undefined} headers
* @returns {FormatterObjects}
*/
function createFormatters(headers) {
const formatters = Object.assign({}, defaultFormatters)

if (!headers) {
return formatters
}

/** @type {string} */
let key

for (key in headers) {
Expand Down
26 changes: 23 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,33 +32,47 @@
"sideEffects": false,
"type": "module",
"main": "index.js",
"types": "index.d.ts",
"files": [
"formatters.d.ts",
"formatters.js",
"index.d.ts",
"index.js"
],
"dependencies": {
"@types/mdast": "^3.0.0",
"is-url": "^1.0.0",
"mdast-util-heading-range": "^3.0.0",
"parse-author": "^2.0.0",
"to-vfile": "^7.0.0",
"type-fest": "^1.0.0",
"unified": "^10.0.0",
"unist-builder": "^3.0.0",
"vfile-find-up": "^6.0.0",
"to-vfile": "^7.0.0"
"vfile": "^5.0.0",
"vfile-find-up": "^6.0.0"
},
"devDependencies": {
"@types/is-url": "^1.2.30",
"@types/parse-author": "^2.0.1",
"@types/tape": "^4.0.0",
"c8": "^7.0.0",
"prettier": "^2.0.0",
"remark": "^14.0.0",
"remark-cli": "^10.0.0",
"remark-gfm": "^1.0.0",
"remark-preset-wooorm": "^8.0.0",
"rimraf": "^3.0.0",
"tape": "^5.0.0",
"type-coverage": "^2.0.0",
"typescript": "^4.0.0",
"xo": "^0.39.0"
},
"scripts": {
"build": "rimraf \"*.d.ts\" && tsc && type-coverage",
"format": "remark . -qfo && prettier . -w --loglevel warn && xo --fix",
"test-api": "node --conditions development test.js",
"test-coverage": "c8 --check-coverage --branches 100 --functions 100 --lines 100 --statements 100 --reporter lcov npm run test-api",
"test": "npm run format && npm run test-coverage"
"test": "npm run build && npm run format && npm run test-coverage"
},
"prettier": {
"tabWidth": 2,
Expand All @@ -76,5 +90,11 @@
"preset-wooorm",
"./index.js"
]
},
"typeCoverage": {
"atLeast": 100,
"detail": true,
"strict": true,
"ignoreCatch": true
}
}
8 changes: 5 additions & 3 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,9 +142,11 @@ Formatters have the following properties:
* `label` — text in the header row that labels the column for this field
* `exclude` — whether to ignore these fields (default: `false`)
* `format` — function called with `value, key, contributor` to format
the value. Expected to return [PhrasingContent][]. Can return null or
undefined (ignored), a string (wrapped in a [text][] node), a string that
looks like a URL (wrapped in a [link][]), one node, or multiple nodes
the value.
Expected to return [PhrasingContent][].
Can return null or undefined (ignored), a string (wrapped in a [text][]
node), a string that looks like a URL (wrapped in a [link][]), one node,
or multiple nodes

##### Notes

Expand Down
Loading

0 comments on commit 4a12931

Please sign in to comment.