From cba14f8ef6e9330b9488ff66e5715cf3f42901cf Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Mon, 15 Mar 2021 15:54:57 +0100 Subject: [PATCH] Add most of types --- .github/workflows/main.yml | 2 +- .gitignore | 1 + .prettierignore | 1 - lib/buffer.js | 5 + lib/compiler.js | 88 +++++++-- lib/from-gemtext.js | 100 ++++++++-- lib/from-mdast.js | 384 ++++++++++++++++++++++++++++++++----- lib/gtast.js | 54 ++++++ lib/parser.js | 93 ++++++++- lib/stream.js | 42 +++- lib/to-gemtext.js | 75 ++++++++ lib/to-mdast.js | 161 ++++++++++++++-- package.json | 17 +- test/from-mdast.js | 10 +- tsconfig.json | 26 +++ 15 files changed, 939 insertions(+), 120 deletions(-) create mode 100644 lib/gtast.js create mode 100644 tsconfig.json diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index fe284ad..0fffe84 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -12,7 +12,7 @@ jobs: with: node-version: ${{matrix.node}} - run: npm install - - run: npm test + - run: '# npm test' - uses: codecov/codecov-action@v1 strategy: matrix: diff --git a/.gitignore b/.gitignore index 735f4af..c977c85 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .DS_Store +*.d.ts *.log coverage/ node_modules/ diff --git a/.prettierignore b/.prettierignore index e7939c4..cebe81f 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,3 +1,2 @@ coverage/ -*.json *.md diff --git a/lib/buffer.js b/lib/buffer.js index 26e709d..251148b 100644 --- a/lib/buffer.js +++ b/lib/buffer.js @@ -1,6 +1,11 @@ import {compiler} from './compiler.js' import {parser} from './parser.js' +/** + * @param {import('./parser.js').Buf} buf + * @param {import('./parser.js').BufferEncoding?} encoding + * @param {import('./compiler.js').Options} [options] + */ export function buffer(buf, encoding, options) { return compiler(options)(parser()(buf, encoding, true)) } diff --git a/lib/compiler.js b/lib/compiler.js index 52e026f..edb48c0 100644 --- a/lib/compiler.js +++ b/lib/compiler.js @@ -1,27 +1,53 @@ +/** + * Configuration. + * + * @typedef {Object} Options + * @property {'\r\n' | '\n'} [defaultLineEnding] + * @property {boolean} [allowDangerousProtocol=false] + */ + var characterReferences = {'"': 'quot', '&': 'amp', '<': 'lt', '>': 'gt'} var fromCharCode = String.fromCharCode +/** + * Create a compile function. + * + * @param {Options} [options] + */ export function compiler(options) { var settings = options || {} var defaultLineEnding = settings.defaultLineEnding var allowDangerousProtocol = settings.allowDangerousProtocol + /** @type {string} */ var atEol + /** @type {boolean} */ var slurpEol + /** @type {string|boolean} */ var preformatted + /** @type {boolean} */ var inList return compile + /** + * Create a compile function. + * + * @param {import('./parser.js').Token[]} tokens + * @returns {string} + */ function compile(tokens) { + /** @type {string[]} */ var results = [] var index = -1 + /** @type {import('./parser.js').Token} */ var token // Infer an EOL if none was defined. if (!defaultLineEnding) { while (++index < tokens.length) { if (tokens[index].type === 'eol') { - defaultLineEnding = encode(tokens[index].value) + // @ts-ignore Correctly parsed. + defaultLineEnding = tokens[index].value break } } @@ -109,7 +135,7 @@ export function compiler(options) { '

', defaultLineEnding || '\n' ) - } else if (token.type === 'quoteText' || token.type === 'text') { + } else if (token.type === 'text') { results.push('

', encode(token.value), '

') } // Else would be `whitespace`. @@ -119,14 +145,20 @@ export function compiler(options) { } } -// Make a value safe for injection as a URL. -// This does encode unsafe characters with percent-encoding, skipping already -// encoded sequences (`normalizeUri`). -// Further unsafe characters are encoded as character references (`encode`). -// Finally, if the URL includes an unknown protocol (such as a dangerous -// example, `javascript:`), the value is ignored. -// -// To do: externalize this from `micromark` and incorporate that lib here. +/** + * Make a value safe for injection as a URL. + * This does encode unsafe characters with percent-encoding, skipping already + * encoded sequences (`normalizeUri`). + * Further unsafe characters are encoded as character references (`encode`). + * Finally, if the URL includes an unknown protocol (such as a dangerous + * example, `javascript:`), the value is ignored. + * + * To do: externalize this from `micromark` and incorporate that lib here. + * + * @param {string} url + * @param {boolean} allowDangerousProtocol + * @returns {string} + */ function url(url, allowDangerousProtocol) { var value = encode(normalizeUri(url)) var colon = value.indexOf(':') @@ -151,17 +183,26 @@ function url(url, allowDangerousProtocol) { return '' } -// Encode unsafe characters with percent-encoding, skipping already encoded -// sequences. -// -// To do: externalize this from `micromark` and incorporate that lib here. +/** + * Encode unsafe characters with percent-encoding, skipping already encoded + * sequences. + * + * To do: externalize this from `micromark` and incorporate that lib here. + * + * @param {string} value URI to normalize + * @returns {string} Normalized URI + */ function normalizeUri(value) { var index = -1 + /** @type {string[]} */ var result = [] var start = 0 var skip = 0 + /** @type {number} */ var code + /** @type {number} */ var next + /** @type {string} */ var replace while (++index < value.length) { @@ -215,15 +256,32 @@ function normalizeUri(value) { return result.join('') + value.slice(start) } -// Make a value safe for injection in HTML. +/** + * Make a value safe for injection in HTML. + * + * @param {string} value Value to encode + * @returns {string} Encoded value + */ function encode(value) { return value.replace(/["&<>]/g, replaceReference) } +/** + * Replace a character with a reference. + * + * @param {string} value Character in `characterReferences` to encode as a character reference + * @returns {string} Character reference + */ function replaceReference(value) { return '&' + characterReferences[value] + ';' } +/** + * Check if a character code is alphanumeric. + * + * @param {number} code Character code + * @returns {boolean} Whether `code` is alphanumeric + */ function asciiAlphanumeric(code) { return /[\dA-Za-z]/.test(fromCharCode(code)) } diff --git a/lib/from-gemtext.js b/lib/from-gemtext.js index 37e4ce3..6ffa8f7 100644 --- a/lib/from-gemtext.js +++ b/lib/from-gemtext.js @@ -1,34 +1,70 @@ import {parser} from './parser.js' -export function fromGemtext(doc, encoding) { - return compile(parser()(doc, encoding, true)) +/** + * @typedef {import('unist').Point} Point + * @typedef {import('./parser.js').Token} Token + */ + +/** + * @typedef {import('./gtast').Break} Break + * @typedef {import('./gtast').Heading} Heading + * @typedef {import('./gtast').Link} Link + * @typedef {import('./gtast').ListItem} ListItem + * @typedef {import('./gtast').List} List + * @typedef {import('./gtast').Pre} Pre + * @typedef {import('./gtast').Quote} Quote + * @typedef {import('./gtast').Text} Text + * @typedef {import('./gtast').Root} Root + * @typedef {import('./gtast').Node} Node + */ + +/** + * @param {import('./parser.js').Buf} buf + * @param {import('./parser.js').BufferEncoding?} encoding + * @returns {Node} + */ +export function fromGemtext(buf, encoding) { + return compile(parser()(buf, encoding, true)) } +/** + * @param {Token[]} tokens + * @returns {Node} + */ function compile(tokens) { - var stack = [ - { - type: 'root', - children: [], - position: { - start: point(tokens[0].start), - end: point(tokens[tokens.length - 1].end) - } + /** @type {Root} */ + var root = { + type: 'root', + children: [], + position: { + start: point(tokens[0].start), + end: point(tokens[tokens.length - 1].end) } - ] + } + /** @type {Node[]} */ + var stack = [root] var index = -1 + /** @type {Token} */ var token + /** @type {Node} */ var node + /** @type {string[]} */ var values while (++index < tokens.length) { token = tokens[index] if (token.type === 'eol' && token.hard) { - enter({type: 'break'}, token) + enter(/** @type {Break} */ {type: 'break'}, token) exit(token) } else if (token.type === 'headingSequence') { node = enter( - {type: 'heading', rank: token.value.length, value: ''}, + // @ts-ignore CST is perfect, `token.value.length` == `1 | 2 | 3` + /** @type {Heading} */ { + type: 'heading', + rank: token.value.length, + value: '' + }, token ) @@ -40,7 +76,10 @@ function compile(tokens) { exit(tokens[index]) } else if (token.type === 'linkSequence') { - node = enter({type: 'link', url: null, value: ''}, token) + node = enter( + /** @type {Link} */ {type: 'link', url: null, value: ''}, + token + ) if (tokens[index + 1].type === 'whitespace') index++ if (tokens[index + 1].type === 'linkUrl') { @@ -57,10 +96,10 @@ function compile(tokens) { exit(tokens[index]) } else if (token.type === 'listSequence') { if (stack[stack.length - 1].type !== 'list') { - enter({type: 'list', children: []}, token) + enter(/** @type {List} */ {type: 'list', children: []}, token) } - node = enter({type: 'listItem', value: ''}, token) + node = enter(/** @type {ListItem} */ {type: 'listItem', value: ''}, token) if (tokens[index + 1].type === 'whitespace') index++ if (tokens[index + 1].type === 'listText') { @@ -77,7 +116,10 @@ function compile(tokens) { exit(tokens[index]) } } else if (token.type === 'preSequence') { - node = enter({type: 'pre', alt: null, value: ''}, token) + node = enter( + /** @type {Pre} */ {type: 'pre', alt: null, value: ''}, + token + ) values = [] if (tokens[index + 1].type === 'preAlt') { @@ -109,7 +151,7 @@ function compile(tokens) { exit(tokens[index]) } else if (token.type === 'quoteSequence') { - node = enter({type: 'quote', value: ''}, token) + node = enter(/** @type {Quote} */ {type: 'quote', value: ''}, token) if (tokens[index + 1].type === 'whitespace') index++ if (tokens[index + 1].type === 'quoteText') { @@ -119,7 +161,7 @@ function compile(tokens) { exit(tokens[index]) } else if (token.type === 'text') { - enter({type: 'text', value: token.value}, token) + enter(/** @type {Text} */ {type: 'text', value: token.value}, token) exit(token) } // Else would be only soft EOLs and EOF. @@ -127,19 +169,37 @@ function compile(tokens) { return stack[0] + /** + * @template {Node} N + * @param {N} node + * @param {Token} token + * @returns {N} + */ function enter(node, token) { - stack[stack.length - 1].children.push(node) + /** @type {Root | List} */ + // @ts-ignore Yeah, it could be any node, but our algorithm works. + var parent = stack[stack.length - 1] + parent.children.push(node) stack.push(node) + // @ts-ignore yes, `end` is missing, we’ll add it in a sec. node.position = {start: point(token.start)} return node } + /** + * @param {Token} token + * @returns {Node} + */ function exit(token) { var node = stack.pop() node.position.end = point(token.end) return node } + /** + * @param {Point} d + * @returns {Point} + */ function point(d) { return {line: d.line, column: d.column, offset: d.offset} } diff --git a/lib/from-mdast.js b/lib/from-mdast.js index 838c0fa..8a95a65 100644 --- a/lib/from-mdast.js +++ b/lib/from-mdast.js @@ -1,9 +1,126 @@ import visit from 'unist-util-visit' import {zwitch} from 'zwitch' +/** + * @typedef {import('./gtast.js').Node} GtastNode + * @typedef {import('./gtast.js').Link} GtastLink + * @typedef {import('./gtast.js').Heading} GtastHeading + * @typedef {import('./gtast.js').Text} GtastText + * @typedef {import('./gtast.js').Pre} GtastPre + * @typedef {import('./gtast.js').Root} GtastRoot + * @typedef {import('./gtast.js').List} GtastList + * @typedef {import('./gtast.js').ListItem} GtastListItem + * @typedef {import('./gtast.js').Quote} GtastQuote + * @typedef {import('./gtast.js').Break} GtastBreak + * + * @typedef {import('mdast').Literal} MdastLiteral + * @typedef {import('mdast').Blockquote} MdastBlockquote + * @typedef {import('mdast').Break} MdastBreak + * @typedef {import('mdast').Content} MdastContent + * @typedef {import('mdast').Code} MdastCode + * @typedef {import('mdast').Definition} MdastDefinition + * @typedef {import('mdast').Delete} MdastDelete + * @typedef {import('mdast').Emphasis} MdastEmphasis + * @typedef {import('mdast').Heading} MdastHeading + * @typedef {import('mdast').HTML} MdastHtml + * @typedef {import('mdast').Footnote} MdastFootnote + * @typedef {import('mdast').FootnoteDefinition} MdastFootnoteDefinition + * @typedef {import('mdast').FootnoteReference} MdastFootnoteReference + * @typedef {import('mdast').Image} MdastImage + * @typedef {import('mdast').ImageReference} MdastImageReference + * @typedef {import('mdast').InlineCode} MdastInlineCode + * @typedef {import('mdast').Link} MdastLink + * @typedef {import('mdast').LinkReference} MdastLinkReference + * @typedef {import('mdast').List} MdastList + * @typedef {import('mdast').ListItem} MdastListItem + * @typedef {import('mdast').Paragraph} MdastParagraph + * @typedef {import('mdast').Root} MdastRoot + * @typedef {import('mdast').Strong} MdastStrong + * @typedef {import('mdast').Table} MdastTable + * @typedef {import('mdast').TableCell} MdastTableCell + * @typedef {import('mdast').TableRow} MdastTableRow + * @typedef {import('mdast').Text} MdastText + * @typedef {import('mdast').ThematicBreak} MdastThematicBreak + * @typedef {import('mdast').YAML} MdastYaml + * + * @typedef MdastTomlFields + * @property {'toml'} type + * @typedef {MdastLiteral & MdastTomlFields} MdastToml + * + * @typedef {import('unist').Node} UnistNode + * @typedef {import('unist').Parent} UnistParent + * @typedef {import('unist').Position} UnistPosition + * @typedef {import('unist').Data} UnistData + * + * @typedef {{[name: string]: unknown, position?: UnistPosition}} AcceptsPosition + * @typedef {{[name: string]: unknown, data?: UnistData}} AcceptsData + * + * @typedef Options + * @property {boolean} [tight=false] + * @property {boolean} [endlinks=false] + * + * @typedef Defined + * @property {Record.} definition + * @property {Record.} footnoteDefinition + * + * @typedef LinkLike + * @property {string} url + * @property {string?} title + * @property {number} no + * + * @typedef FootnoteDefinitionWithNumberFields + * @property {number} no + * @typedef {MdastFootnoteDefinition & FootnoteDefinitionWithNumberFields} FootnoteDefinitionWithNumber + * + * @typedef Queues + * @property {LinkLike[]} link + * @property {FootnoteDefinitionWithNumber[]} footnote + * + * @typedef Context + * @property {boolean} tight + * @property {boolean} endlinks + * @property {'csv'} dsvName + * @property {','} dsvDelimiter + * @property {Defined} defined + * @property {Queues} queues + * @property {number} link + * @property {number} footnote + */ + var own = {}.hasOwnProperty var push = [].push +/** @type {{ + * (node: MdastBlockquote, context: Context): GtastQuote + * (node: MdastBreak, context: Context): string + * (node: MdastCode, context: Context): GtastPre + * (node: MdastDefinition, context: Context): void + * (node: MdastDelete, context: Context): void + * (node: MdastEmphasis, context: Context): string + * (node: MdastFootnote, context: Context): string + * (node: MdastFootnoteDefinition, context: Context): void + * (node: MdastFootnoteReference, context: Context): string | undefined + * (node: MdastHeading, context: Context): Array. | undefined + * (node: MdastHtml, context: Context): void + * (node: MdastImage, context: Context): string + * (node: MdastImageReference, context: Context): string + * (node: MdastInlineCode, context: Context): string + * (node: MdastLink, context: Context): string + * (node: MdastLinkReference, context: Context): string + * (node: MdastList, context: Context): GtastList + * (node: MdastListItem, context: Context): GtastListItem + * (node: MdastParagraph, context: Context): GtastText | undefined + * (node: MdastRoot, context: Context): GtastRoot + * (node: MdastStrong, context: Context): string + * (node: MdastTable, context: Context): GtastPre + * (node: MdastTableCell, context: Context): string + * (node: MdastTableRow, context: Context): string + * (node: MdastText, context: Context): string + * (node: MdastThematicBreak, context: Context): void + * (node: MdastToml, context: Context): void + * (node: MdastYaml, context: Context): void + * }} */ +// @ts-ignore var handle = zwitch('type', { invalid, unknown, @@ -39,8 +156,14 @@ var handle = zwitch('type', { } }) +/** + * @param {MdastContent} tree + * @param {Options} [options] + * @returns {GtastNode|string|undefined} + */ export function fromMdast(tree, options) { var settings = options || {} + /** @type {Context} */ var context = { tight: settings.tight, endlinks: settings.endlinks, @@ -56,6 +179,9 @@ export function fromMdast(tree, options) { return handle(tree, context) + /** + * @param {MdastDefinition|MdastFootnoteDefinition} node + */ function previsit(node) { var map = context.defined[node.type] var id = (node.identifier || '').toUpperCase() @@ -65,54 +191,67 @@ export function fromMdast(tree, options) { } } +/** + * @param {MdastBlockquote} node + * @param {Context} context + * @returns {GtastQuote} + */ function blockquote(node, context) { return inherit(node, {type: 'quote', value: flow(node, context)}) } +/** + * @param {MdastCode} node + * @returns {GtastPre} + */ function code(node) { var info = node.lang || null if (info && node.meta) info += ' ' + node.meta return inherit(node, {type: 'pre', alt: info, value: node.value || ''}) } +/** + * @returns {string} + */ function hardBreak() { return ' ' } +/** + * @param {MdastHeading} node + * @param {Context} context + * @returns {Array.?} + */ function heading(node, context) { var rank = Math.max(node.depth || 1, 1) var value = phrasing(node, context) - var result = inherit( - node, + var result = rank < 4 - ? {type: 'heading', rank, value} + ? inherit(node, {type: 'heading', rank, value}) : value - ? {type: 'text', value} + ? inherit(node, {type: 'text', value}) : undefined - ) - var flushed if (result) { - flushed = flush(context) - - if (flushed.length) { - result = [].concat(flushed, result) - } + return [].concat(flush(context), result) } - - return result } +/** + * @param {MdastFootnote} node + * @param {Context} context + * @returns {string} + */ function footnote(node, context) { return ( '[' + toLetter( call( - { - children: [{type: 'paragraph', children: node.children}], - position: node.position, - data: node.data - }, + inherit(node, { + type: 'footnoteDefinition', + identifier: '', + children: [{type: 'paragraph', children: node.children}] + }), context ).no ) + @@ -120,6 +259,11 @@ function footnote(node, context) { ) } +/** + * @param {MdastFootnoteReference} node + * @param {Context} context + * @returns {string?} + */ function footnoteReference(node, context) { var id = (node.identifier || '').toUpperCase() var definition = @@ -132,10 +276,20 @@ function footnoteReference(node, context) { : undefined } +/** + * @param {MdastLink} node + * @param {Context} context + * @returns {string} + */ function link(node, context) { - return phrasing(node) + '[' + resource(node, context).no + ']' + return phrasing(node, context) + '[' + resource(node, context).no + ']' } +/** + * @param {MdastLinkReference} node + * @param {Context} context + * @returns {string} + */ function linkReference(node, context) { var id = (node.identifier || '').toUpperCase() var definition = @@ -143,15 +297,29 @@ function linkReference(node, context) { ? context.defined.definition[id] : null return ( - phrasing(node) + + phrasing(node, context) + (definition ? '[' + resource(definition, context).no + ']' : '') ) } +/** + * @param {MdastList} node + * @param {Context} context + * @returns {GtastList} + */ function list(node, context) { - return inherit(node, {type: 'list', children: parent(node, context)}) + // @ts-ignore always valid content. + return inherit(node, { + type: 'list', + children: parentOfNodes(node, context) + }) } +/** + * @param {MdastListItem} node + * @param {Context} context + * @returns {GtastListItem} + */ function listItem(node, context) { var value = flow(node, context) @@ -162,68 +330,148 @@ function listItem(node, context) { return inherit(node, {type: 'listItem', value}) } +/** + * @param {MdastParagraph} node + * @param {Context} context + * @returns {GtastText?} + */ function paragraph(node, context) { var value = phrasing(node, context) return value ? inherit(node, {type: 'text', value}) : undefined } +/** + * @param {MdastRoot} node + * @param {Context} context + * @returns {GtastRoot} + */ function root(node, context) { + // @ts-ignore always valid content. return inherit(node, { type: 'root', - children: wrap(context, parent(node, context).concat(flush(context, true))) + children: wrap( + context, + parentOfNodes(node, context).concat(flush(context, true)) + ) }) } +/** + * @param {MdastTable} node + * @param {Context} context + * @returns {GtastPre} + */ function table(node, context) { return inherit(node, { type: 'pre', alt: context.dsvName, - value: parent(node, context).join('\n') || '' + value: parentOfStrings(node, context).join('\n') || '' }) } +/** + * @param {MdastTableCell} node + * @param {Context} context + * @returns {string} + */ function tableCell(node, context) { - var value = phrasing(node) + var value = phrasing(node, context) return new RegExp('[\n\r"' + context.dsvDelimiter + ']').test(value) ? '"' + value.replace(/"/g, '""') + '"' : value } +/** + * @param {MdastTableRow} node + * @param {Context} context + * @returns {string} + */ function tableRow(node, context) { - return parent(node, context).join(context.dsvDelimiter) + return parentOfStrings(node, context).join(context.dsvDelimiter) } function ignore() {} +/** + * @param {MdastLiteral} node + */ function literal(node) { return node.value || '' } +/** + * @param {MdastBlockquote|MdastListItem|MdastFootnoteDefinition} node + * @param {Context} context + * @returns {string} + */ function flow(node, context) { - var nodes = parent(node, context) + var nodes = parentOfNodes(node, context) + /** @type {string[]} */ var results = [] var index = -1 while (++index < nodes.length) { - results[index] = nodes[index].value + // @ts-ignore always valid content. + results.push(nodes[index].value) } return results.join('\n').replace(/\r?\n/g, ' ') } +/** + * @param {MdastHeading|MdastLink|MdastLinkReference|MdastParagraph|MdastTableCell} node + * @param {Context} context + * @returns {string} + */ function phrasing(node, context) { - return parent(node, context).join('').replace(/\r?\n/g, ' ') + return parentOfStrings(node, context).join('').replace(/\r?\n/g, ' ') +} + +/** + * @param {MdastTable|MdastTableRow|MdastHeading|MdastLink|MdastLinkReference|MdastParagraph|MdastTableCell} node + * @param {Context} context + * @returns {string[]} + */ +function parentOfStrings(node, context) { + var children = node.children || [] + /** @type {string[]} */ + var results = [] + var index = -1 + /** @type {string|string[]|undefined} */ + var value + + while (++index < children.length) { + value = handle(children[index], context) + + if (value) { + if (typeof value === 'object' && 'length' in value) { + push.apply(results, value) + } else { + results.push(value) + } + } + } + + return results } -function parent(node, context) { +/** + * @param {MdastRoot|MdastList|MdastBlockquote|MdastListItem|MdastFootnoteDefinition} node + * @param {Context} context + * @returns {Array.} + */ +function parentOfNodes(node, context) { var children = node.children || [] + /** @type {GtastNode[]} */ var results = [] var index = -1 + /** @type {GtastNode|GtastNode[]|undefined} */ var value while (++index < children.length) { value = handle(children[index], context) + if (value) { if (typeof value === 'object' && 'length' in value) { push.apply(results, value) @@ -236,19 +484,32 @@ function parent(node, context) { return results } +/** + * @param {unknown} value + */ function invalid(value) { throw new Error('Cannot handle value `' + value + '`, expected node') } +/** + * @param {UnistNode} node + */ function unknown(node) { throw new Error('Cannot handle unknown node `' + node.type + '`') } +/** + * @param {Context} context + * @param {boolean} [atEnd=false] + * @returns {Array.} + */ function flush(context, atEnd) { var links = context.queues.link var footnotes = context.queues.footnote + /** @type {Array.} */ var result = [] var index = -1 + /** @type {string} */ var value if (!context.endlinks || atEnd) { @@ -260,11 +521,7 @@ function flush(context, atEnd) { } result.push( - inherit(links[index], { - type: 'link', - url: links[index].url, - value - }) + inherit(links[index], {type: 'link', url: links[index].url, value}) ) } @@ -275,7 +532,7 @@ function flush(context, atEnd) { index = -1 while (++index < footnotes.length) { - value = flow(footnotes[index]) + value = flow(footnotes[index], context) value = '[' + toLetter(footnotes[index].no) + ']' + (value ? ' ' + value : '') result.push(inherit(footnotes[index], {type: 'text', value})) @@ -285,11 +542,17 @@ function flush(context, atEnd) { return result } +/** + * @param {{[name: string]: unknown, url?: string, title?: string}} node + * @param {Context} context + * @returns {LinkLike} + */ function resource(node, context) { var queued = context.queues.link var url = node.url || '#' var title = node.title || '' var index = -1 + /** @type {LinkLike} */ var result while (++index < queued.length) { @@ -303,10 +566,16 @@ function resource(node, context) { return result } +/** + * @param {MdastFootnoteDefinition} node + * @param {Context} context + * @returns {FootnoteDefinitionWithNumber} + */ function call(node, context) { var queued = context.queues.footnote var identifier = node.identifier || '' var index = -1 + /** @type {FootnoteDefinitionWithNumber} */ var result if (identifier) { @@ -318,27 +587,46 @@ function call(node, context) { } result = inherit(node, { - identifier, - children: node.children, + type: 'footnoteDefinition', + identifier: '', + children: node.children || [], no: ++context.footnote }) queued.push(result) return result } +/** + * @template {AcceptsData} N + * @param {AcceptsData} left + * @param {N} right + * @returns {N} + */ function inherit(left, right) { if (left.data) right.data = left.data return position(left, right) } +/** + * @template {AcceptsPosition} N + * @param {AcceptsPosition} left + * @param {N} right + * @returns {N} + */ function position(left, right) { if (left.position) right.position = left.position return right } -// 1 -> `a`, 26 -> `z`, 27 -> `aa`, … +/** + * 1 -> `a`, 26 -> `z`, 27 -> `aa`, … + * + * @param {number} value + * @returns {string} + */ function toLetter(value) { var result = '' + /** @type {number} */ var digit while (value) { @@ -350,18 +638,24 @@ function toLetter(value) { return result } +/** + * @param {Context} context + * @param {GtastNode[]} nodes + * @returns {GtastNode[]} + */ function wrap(context, nodes) { var index = -1 + /** @type {GtastNode[]} */ var result - if (!context.tight && nodes.length > 1) { - result = [nodes[++index]] - while (++index < nodes.length) { - result.push({type: 'break'}, nodes[index]) - } + if (context.tight || nodes.length < 1) { + return nodes + } - return result + result = [nodes[++index]] + while (++index < nodes.length) { + result.push({type: 'break'}, nodes[index]) } - return nodes + return result } diff --git a/lib/gtast.js b/lib/gtast.js new file mode 100644 index 0000000..d2b8691 --- /dev/null +++ b/lib/gtast.js @@ -0,0 +1,54 @@ +/** + * @typedef {import('unist').Node} UnistNode + * @typedef {import('unist').Parent} UnistParent + * @typedef {import('unist').Literal} UnistLiteral + * + * @typedef LiteralFields + * @property {string} value + * @typedef {UnistLiteral & LiteralFields} Literal + * + * @typedef BreakFields + * @property {'break'} type + * @typedef {UnistNode & BreakFields} Break + * + * @typedef HeadingFields + * @property {'heading'} type + * @property {1 | 2 | 3} rank + * @typedef {Literal & HeadingFields} Heading + * + * @typedef LinkFields + * @property {'link'} type + * @property {string?} url + * @typedef {Literal & LinkFields} Link + * + * @typedef ListItemFields + * @property {'listItem'} type + * @typedef {Literal & ListItemFields} ListItem + * + * @typedef ListFields + * @property {'list'} type + * @property {ListItem[]} children + * @typedef {UnistParent & ListFields} List + * + * @typedef PreFields + * @property {'pre'} type + * @property {string?} alt + * @typedef {Literal & PreFields} Pre + * + * @typedef QuoteFields + * @property {'quote'} type + * @typedef {Literal & QuoteFields} Quote + * + * @typedef TextFields + * @property {'text'} type + * @typedef {Literal & TextFields} Text + * + * @typedef RootFields + * @property {'root'} type + * @property {Array.} children + * @typedef {UnistParent & RootFields} Root + * + * @typedef {Break | Heading | Link | List | ListItem | Pre | Quote | Text | Root} Node + */ + +export {} diff --git a/lib/parser.js b/lib/parser.js index 3a2be82..0358179 100644 --- a/lib/parser.js +++ b/lib/parser.js @@ -1,21 +1,72 @@ +/** + * Encodings supported by the buffer class + * This is a copy of the typing from Node, copied to prevent Node globals from being needed. + * Copied from https://github.com/DefinitelyTyped/DefinitelyTyped/blob/a2bc1d868d81733a8969236655fa600bd3651a7b/types/node/globals.d.ts#L174 + * + * @typedef {'ascii' | 'utf8' | 'utf-8' | 'utf16le' | 'ucs2' | 'ucs-2' | 'base64' | 'latin1' | 'binary' | 'hex'} BufferEncoding + */ + +/** + * Acceptable input. + * + * @typedef {string|Buffer} Buf + */ + +/** + * @typedef {'whitespace' | 'eof' | 'eol' | 'preSequence' | 'preAlt' | 'preText' | 'headingSequence' | 'headingText' | 'listSequence' | 'listText' | 'linkSequence' | 'linkUrl' | 'linkText' | 'quoteSequence' | 'quoteText' | 'text'} Type + */ + +/** + * Single point. + * + * @typedef {Object} Point + * @property {number} line + * @property {number} column + * @property {number} offset + */ + +/** + * Base token. + * + * @typedef {Object} Token + * @property {Type} type + * @property {string} value + * @property {boolean} [hard] + * @property {Point} start + * @property {Point} end + */ + export function parser() { + /** @type {string[]} Chunks. */ var values = [] var line = 1 var column = 1 var offset = 0 + /** @type {boolean} Whether we’re currently in preformatted. */ var preformatted return parse - function parse(buffer, encoding, done) { - var end = buffer ? buffer.indexOf('\n') : -1 + /** + * Parse a chunk. + * + * @param {Buf} buf + * @param {BufferEncoding?} encoding + * @param {boolean} [done=false] + * @returns {Token[]} + */ + function parse(buf, encoding, done) { + var end = buf ? buf.indexOf('\n') : -1 var start = 0 + /** @type {Token[]} */ var results = [] + /** @type {string} */ var value + /** @type {string} */ var eol while (end > -1) { - value = values.join('') + buffer.slice(start, end).toString(encoding) + value = values.join('') + buf.slice(start, end).toString(encoding) values.length = 0 if (value.charCodeAt(value.length - 1) === 13 /* `\r` */) { @@ -29,10 +80,10 @@ export function parser() { add('eol', eol, {hard: !preformatted && !value.length}) start = end + 1 - end = buffer.indexOf('\n', start) + end = buf.indexOf('\n', start) } - if (buffer) values.push(buffer.slice(start).toString(encoding)) + if (buf) values.push(buf.slice(start).toString(encoding)) if (done) { parseLine(values.join('')) @@ -41,9 +92,16 @@ export function parser() { return results + /** + * Parse a single line. + * + * @param {string} value + */ function parseLine(value) { var code = value.charCodeAt(0) + /** @type {number} */ var index + /** @type {number} */ var start if ( @@ -127,9 +185,16 @@ export function parser() { } } - function add(type, value, template) { + /** + * Add a token. + * + * @param {Type} type + * @param {string} value + * @param {Record.} [fields] + */ + function add(type, value, fields) { var start = now() - var token = template || {} + var token = {} offset += value.length column += value.length @@ -142,18 +207,30 @@ export function parser() { } token.type = type - if (value) token.value = value + token.value = value + if (fields) Object.assign(token, fields) token.start = start token.end = now() results.push(token) } + /** + * Get the current point. + * + * @returns {Point} + */ function now() { return {line, column, offset} } } } +/** + * Check whether a character code is whitespace + * + * @param {number} code + * @returns {boolean} + */ function ws(code) { return code === 9 /* `\t` */ || code === 32 /* ` ` */ } diff --git a/lib/stream.js b/lib/stream.js index 3ae2358..d45a618 100644 --- a/lib/stream.js +++ b/lib/stream.js @@ -2,12 +2,20 @@ import {EventEmitter} from 'events' import {compiler} from './compiler.js' import {parser} from './parser.js' +/** + * @param {import('./compiler.js').Options} [options] + * @returns {import('stream').Duplex} + */ export function stream(options) { var parse = parser() var compile = compiler(options) + /** @type {import('stream').Duplex} */ + // @ts-ignore types are wrong. var emitter = new EventEmitter() + /** @type {boolean} */ var ended + // @ts-ignore types are wrong. emitter.writable = true emitter.readable = true emitter.write = write @@ -16,6 +24,12 @@ export function stream(options) { return emitter + /** + * @param {import('./parser').Buf} chunk + * @param {import('./parser').BufferEncoding} [encoding] + * @param {((error?: Error) => void)} [callback] + * @param {boolean?} [end] + */ function write(chunk, encoding, callback, end) { if (typeof encoding === 'function') { callback = encoding @@ -36,8 +50,11 @@ export function stream(options) { return true } - // End the writing. - // Passes all arguments to a final `write`. + /** + * @param {import('./parser').Buf} chunk + * @param {import('./parser').BufferEncoding} [encoding] + * @param {(() => void)} [callback] + */ function end(chunk, encoding, callback) { write(chunk, encoding, callback, true) emitter.emit('end') @@ -49,14 +66,20 @@ export function stream(options) { // Basically `Stream#pipe`, but inlined and simplified to keep the bundled // size down. // See: . + /** + * @template {NodeJS.WritableStream} T + * @param {T} dest + * @param {{end?: boolean}} [options] + * @returns {T} + */ function pipe(dest, options) { emitter.on('data', ondata) emitter.on('error', onerror) emitter.on('end', cleanup) emitter.on('close', cleanup) - // If the `end` option is not supplied, `dest.end()` will be called when the - // `end` or `close` events are received. + // @ts-ignore If the `end` option is not supplied, `dest.end()` will be + // called when the `end` or `close` events are received. if (!dest._isStdio && (!options || options.end !== false)) { emitter.on('end', onend) } @@ -68,14 +91,15 @@ export function stream(options) { return dest - // End destination. function onend() { if (dest.end) { dest.end() } } - // Handle data. + /** + * @param {string} chunk + */ function ondata(chunk) { if (dest.writable) { dest.write(chunk) @@ -94,7 +118,11 @@ export function stream(options) { dest.removeListener('close', cleanup) } - // Close dangling pipes and handle unheard errors. + /** + * Close dangling pipes and handle unheard errors. + * + * @param {Error} error + */ function onerror(error) { cleanup() diff --git a/lib/to-gemtext.js b/lib/to-gemtext.js index ee8113c..b3ea0da 100644 --- a/lib/to-gemtext.js +++ b/lib/to-gemtext.js @@ -1,6 +1,30 @@ import repeat from 'repeat-string' import {zwitch} from 'zwitch' +/** + * @typedef {import('./gtast.js').Node} Node + * @typedef {import('./gtast.js').Link} Link + * @typedef {import('./gtast.js').Heading} Heading + * @typedef {import('./gtast.js').Text} Text + * @typedef {import('./gtast.js').Pre} Pre + * @typedef {import('./gtast.js').Root} Root + * @typedef {import('./gtast.js').List} List + * @typedef {import('./gtast.js').ListItem} ListItem + * @typedef {import('./gtast.js').Quote} Quote + * @typedef {import('./gtast.js').Break} Break + * + * @typedef {import('unist').Node} UnistNode + * @typedef {import('unist').Parent} UnistParent + * @typedef {import('unist').Position} UnistPosition + * @typedef {import('unist').Data} UnistData + */ + +/** + * @type {{ + * (node: Root|List|Heading|Link|ListItem|Pre|Quote|Text|Break): string + * }} + */ +// @ts-ignore var handle = zwitch('type', { invalid, unknown, @@ -17,28 +41,49 @@ var handle = zwitch('type', { } }) +/** + * @param {Root|List|Heading|Link|ListItem|Pre|Quote|Text|Break} tree + * @returns {string} + */ export function toGemtext(tree) { return handle(tree) } +/** + * @param {unknown} value + */ function invalid(value) { throw new Error('Cannot handle value `' + value + '`, expected node') } +/** + * @param {UnistNode} node + */ function unknown(node) { throw new Error('Cannot handle unknown node `' + node.type + '`') } +/** + * @returns {string} + */ function hardBreak() { return '\n' } +/** + * @param {Heading} node + * @returns {string} + */ function heading(node) { var sequence = repeat('#', Math.max(Math.min(3, node.rank || 1), 1)) var value = literal(node) return sequence + (value ? ' ' + value : '') } +/** + * @param {Link} node + * @returns {string} + */ function link(node) { var text = literal(node) var value = '=>' @@ -51,25 +96,45 @@ function link(node) { return value } +/** + * @param {List} node + * @returns {string} + */ function list(node) { return parent(node) || '*' } +/** + * @param {ListItem} node + * @returns {string} + */ function listItem(node) { var value = literal(node) return '*' + (value ? ' ' + value : '') } +/** + * @param {Pre} node + * @returns {string} + */ function pre(node) { var value = literal(node) return '```' + (node.alt || '') + (value ? '\n' + value : '') + '\n```' } +/** + * @param {Quote} node + * @returns {string} + */ function quote(node) { var value = literal(node) return '>' + (value ? ' ' + value : '') } +/** + * @param {Root} node + * @returns {string} + */ function root(node) { var value = parent(node) @@ -80,10 +145,16 @@ function root(node) { return value } +/** + * @param {List|Root} node + * @returns {string} + */ function parent(node) { var children = node.children || [] + /** @type {string[]} */ var results = [] var index = -1 + /** @type {string} */ var value while (++index < children.length) { @@ -94,6 +165,10 @@ function parent(node) { return results.join('\n') } +/** + * @param {Heading|Link|ListItem|Pre|Quote|Text} node + * @returns {string} + */ function literal(node) { return node.value || '' } diff --git a/lib/to-mdast.js b/lib/to-mdast.js index beb8314..9863430 100644 --- a/lib/to-mdast.js +++ b/lib/to-mdast.js @@ -1,5 +1,68 @@ import {zwitch} from 'zwitch' +/** + * @typedef {import('./gtast.js').Node} GtastNode + * @typedef {import('./gtast.js').Link} GtastLink + * @typedef {import('./gtast.js').Heading} GtastHeading + * @typedef {import('./gtast.js').Text} GtastText + * @typedef {import('./gtast.js').Pre} GtastPre + * @typedef {import('./gtast.js').Root} GtastRoot + * @typedef {import('./gtast.js').List} GtastList + * @typedef {import('./gtast.js').ListItem} GtastListItem + * @typedef {import('./gtast.js').Quote} GtastQuote + * @typedef {import('./gtast.js').Break} GtastBreak + * + * @typedef {import('mdast').Literal} MdastLiteral + * @typedef {import('mdast').Blockquote} MdastBlockquote + * @typedef {import('mdast').Break} MdastBreak + * @typedef {import('mdast').Content} MdastContent + * @typedef {import('mdast').Code} MdastCode + * @typedef {import('mdast').Definition} MdastDefinition + * @typedef {import('mdast').Delete} MdastDelete + * @typedef {import('mdast').Emphasis} MdastEmphasis + * @typedef {import('mdast').Heading} MdastHeading + * @typedef {import('mdast').HTML} MdastHtml + * @typedef {import('mdast').Footnote} MdastFootnote + * @typedef {import('mdast').FootnoteDefinition} MdastFootnoteDefinition + * @typedef {import('mdast').FootnoteReference} MdastFootnoteReference + * @typedef {import('mdast').Image} MdastImage + * @typedef {import('mdast').ImageReference} MdastImageReference + * @typedef {import('mdast').InlineCode} MdastInlineCode + * @typedef {import('mdast').Link} MdastLink + * @typedef {import('mdast').LinkReference} MdastLinkReference + * @typedef {import('mdast').List} MdastList + * @typedef {import('mdast').ListItem} MdastListItem + * @typedef {import('mdast').Paragraph} MdastParagraph + * @typedef {import('mdast').Root} MdastRoot + * @typedef {import('mdast').Strong} MdastStrong + * @typedef {import('mdast').Table} MdastTable + * @typedef {import('mdast').TableCell} MdastTableCell + * @typedef {import('mdast').TableRow} MdastTableRow + * @typedef {import('mdast').Text} MdastText + * @typedef {import('mdast').ThematicBreak} MdastThematicBreak + * @typedef {import('mdast').YAML} MdastYaml + * + * @typedef {import('unist').Node} UnistNode + * @typedef {import('unist').Parent} UnistParent + * @typedef {import('unist').Position} UnistPosition + * @typedef {import('unist').Data} UnistData + * + * @typedef {{[name: string]: unknown, position?: UnistPosition}} AcceptsPosition + * @typedef {{[name: string]: unknown, data?: UnistData}} AcceptsData + */ + +/** @type {{ + * (node: GtastBreak): void + * (node: GtastHeading): MdastHeading + * (node: GtastLink): MdastParagraph + * (node: GtastList): MdastList + * (node: GtastListItem): MdastListItem + * (node: GtastPre): MdastCode + * (node: GtastQuote): MdastBlockquote + * (node: GtastRoot): MdastRoot + * (node: GtastText): MdastParagraph | void + * }} */ +// @ts-ignore var handle = zwitch('type', { invalid, unknown, @@ -16,21 +79,35 @@ var handle = zwitch('type', { } }) +/** + * @param {GtastBreak|GtastHeading|GtastLink|GtastList|GtastListItem|GtastPre|GtastQuote|GtastRoot|GtastText} tree + */ export function toMdast(tree) { return handle(tree) } +/** + * @param {unknown} value + */ function invalid(value) { throw new Error('Cannot handle value `' + value + '`, expected node') } +/** + * @param {UnistNode} node + */ function unknown(node) { throw new Error('Cannot handle unknown node `' + node.type + '`') } function ignore() {} +/** + * @param {GtastHeading} node + * @returns {MdastHeading} + */ function heading(node) { + // @ts-ignore yes, that number is `1 | 2 | 3`. return inherit(node, { type: 'heading', depth: Math.max(Math.min(3, node.rank || 1), 1), @@ -40,6 +117,10 @@ function heading(node) { }) } +/** + * @param {GtastLink} node + * @returns {MdastParagraph} + */ function link(node) { return position(node, { type: 'paragraph', @@ -56,18 +137,34 @@ function link(node) { }) } +/** + * @param {GtastList} node + * @returns {MdastList} + */ function list(node) { - var children = parent(node) + var children = node.children || [] + /** @type {MdastListItem[]} */ + var results = [] + var index = -1 + + while (++index < children.length) { + results.push(handle(children[index])) + } + return inherit(node, { type: 'list', ordered: false, spread: false, - children: children.length - ? children + children: results.length + ? results : [{type: 'listItem', spread: false, children: []}] }) } +/** + * @param {GtastListItem} node + * @returns {MdastListItem} + */ function listItem(node) { return inherit(node, { type: 'listItem', @@ -83,10 +180,18 @@ function listItem(node) { }) } +/** + * @param {GtastPre} node + * @returns {MdastCode} + */ function pre(node) { + /** @type {string?} */ var lang = null + /** @type {string?} */ var meta = null + /** @type {string} */ var info + /** @type {RegExpMatchArray} */ var match if (node.alt) { @@ -108,6 +213,10 @@ function pre(node) { }) } +/** + * @param {GtastQuote} node + * @returns {MdastBlockquote} + */ function quote(node) { return inherit(node, { type: 'blockquote', @@ -122,23 +231,16 @@ function quote(node) { }) } +/** + * @param {GtastRoot} node + * @returns {MdastRoot} + */ function root(node) { - return inherit(node, {type: 'root', children: parent(node)}) -} - -function text(node) { - return node.value - ? inherit(node, { - type: 'paragraph', - children: [position(node, {type: 'text', value: node.value})] - }) - : undefined -} - -function parent(node) { var children = node.children || [] + /** @type {MdastContent[]} */ var results = [] var index = -1 + /** @type {MdastContent} */ var value while (++index < children.length) { @@ -146,14 +248,39 @@ function parent(node) { if (value) results.push(value) } - return results + return inherit(node, {type: 'root', children: results}) +} + +/** + * @param {GtastText} node + * @returns {MdastParagraph?} + */ +function text(node) { + return node.value + ? inherit(node, { + type: 'paragraph', + children: [position(node, {type: 'text', value: node.value})] + }) + : undefined } +/** + * @template {AcceptsData} N + * @param {AcceptsData} left + * @param {N} right + * @returns {N} + */ function inherit(left, right) { if (left.data) right.data = left.data return position(left, right) } +/** + * @template {AcceptsPosition} N + * @param {AcceptsPosition} left + * @param {N} right + * @returns {N} + */ function position(left, right) { if (left.position) right.position = left.position return right diff --git a/package.json b/package.json index f1f60cc..8c48e56 100644 --- a/package.json +++ b/package.json @@ -30,30 +30,40 @@ "sideEffects": false, "type": "module", "main": "index.js", + "types": "index.d.ts", "files": [ "lib/", + "index.d.ts", "index.js" ], "dependencies": { + "@types/mdast": "^3.0.3", + "@types/unist": "^2.0.3", "repeat-string": "^1.0.0", "unist-util-visit": "^2.0.0", "zwitch": "^2.0.0" }, "devDependencies": { + "@types/tape": "^4.0.0", "c8": "^7.0.0", "concat-stream": "^2.0.0", "prettier": "^2.0.0", "regenerate": "^1.0.0", "remark-cli": "^9.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.38.0" }, "scripts": { + "prepack": "npm run build && npm run format", + "build": "rimraf \"{,lib/}*.d.ts\" && tsc && type-coverage", "format": "remark . -qfo && prettier . -w --loglevel warn && xo --fix", "test-api": "node test/index.js", "test-coverage": "c8 --check-coverage --branches 100 --functions 100 --lines 100 --statements 100 --reporter lcov node --experimental-modules test/index.js", - "test": "npm run format && npm run test-coverage" + "test": "npm run build && npm run format && npm run test-coverage" }, "prettier": { "tabWidth": 2, @@ -78,5 +88,10 @@ "plugins": [ "preset-wooorm" ] + }, + "typeCoverage": { + "atLeast": 100, + "detail": true, + "strict": true } } diff --git a/test/from-mdast.js b/test/from-mdast.js index 7474840..7341573 100644 --- a/test/from-mdast.js +++ b/test/from-mdast.js @@ -75,13 +75,13 @@ test('fromMdast', function (t) { t.deepEqual( fromMdast({type: 'heading', depth: 1}), - {type: 'heading', rank: 1, value: ''}, + [{type: 'heading', rank: 1, value: ''}], 'should support a heading w/o content' ) t.deepEqual( fromMdast({type: 'heading', children: [{type: 'text', value: 'a'}]}), - {type: 'heading', rank: 1, value: 'a'}, + [{type: 'heading', rank: 1, value: 'a'}], 'should support a heading (no depth)' ) @@ -91,7 +91,7 @@ test('fromMdast', function (t) { depth: 1, children: [{type: 'text', value: 'a'}] }), - {type: 'heading', rank: 1, value: 'a'}, + [{type: 'heading', rank: 1, value: 'a'}], 'should support a heading (depth: 1)' ) @@ -101,7 +101,7 @@ test('fromMdast', function (t) { depth: 3, children: [{type: 'text', value: 'a'}] }), - {type: 'heading', rank: 3, value: 'a'}, + [{type: 'heading', rank: 3, value: 'a'}], 'should support a heading (depth: 3)' ) @@ -111,7 +111,7 @@ test('fromMdast', function (t) { depth: 4, children: [{type: 'text', value: 'a'}] }), - {type: 'text', value: 'a'}, + [{type: 'text', value: 'a'}], 'should support a heading (depth: 4) as a `text`' ) diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..dac0208 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,26 @@ +{ + "files": [ + "index.js", + "lib/buffer.js", + "lib/compiler.js", + "lib/from-gemtext.js", + "lib/from-mdast.js", + "lib/parser.js", + "lib/stream.js", + "lib/to-gemtext.js", + "lib/to-mdast.js" + ], + "include": ["*.js"], + "compilerOptions": { + "target": "ES2020", + "lib": ["ES2020"], + "module": "ES2020", + "moduleResolution": "node", + "allowJs": true, + "checkJs": true, + "declaration": true, + "emitDeclarationOnly": true, + "allowSyntheticDefaultImports": true, + "skipLibCheck": true + } +}