From 8764d6bcff3bcd371cd7576b455bf9de5b836fc7 Mon Sep 17 00:00:00 2001 From: Lloyd Brookes Date: Thu, 29 Aug 2024 15:41:54 +0100 Subject: [PATCH] Support clever-links, monospace-lines, {@linkcode}, {@linkplain}. Merges PR #86 --- helpers/ddata.js | 46 ++++++-- helpers/helpers.js | 8 +- lib/dmd-options.js | 14 +++ package-lock.json | 16 ++- package.json | 1 + test/ddata/parseLink.js | 256 ++++++++++++++++++++++++++++++++++++++-- 6 files changed, 310 insertions(+), 31 deletions(-) diff --git a/helpers/ddata.js b/helpers/ddata.js index ced9d6f..3bfca97 100644 --- a/helpers/ddata.js +++ b/helpers/ddata.js @@ -6,6 +6,7 @@ const objectGet = require('object-get') const where = require('test-value').where const flatten = require('reduce-flatten') const state = require('../lib/state') +const urlRe = require('regex-repo').urlRe let malformedDataWarningIssued = false @@ -620,23 +621,25 @@ function methodSig () { /** * extracts url and caption data from @link tags * @param {string} - a string containing one or more {@link} tags - * @returns {Array.<{original: string, caption: string, url: string}>} + * @param {object} - `dmdOptions`; link formatting is influenced by the `clever-links` and `monospace-links` values + * @returns {Array.<{original: string, caption: string, url: string, format: 'code'|'plain'}>} * @static */ -function parseLink (text) { +function parseLink (text, dmdOptions = {}) { if (!text) return '' const results = [] let matches = null - const link1 = /{@link\s+([^\s}|]+?)\s*}/g // {@link someSymbol} - const link2 = /\[([^\]]+?)\]{@link\s+([^\s}|]+?)\s*}/g // [caption here]{@link someSymbol} - const link3 = /{@link\s+([^\s}|]+?)\s*\|([^}]+?)}/g // {@link someSymbol|caption here} - const link4 = /{@link\s+([^\s}|]+?)\s+([^}|]+?)}/g // {@link someSymbol Caption Here} + const link1 = /{@link(code|plain)?\s+([^\s}|]+?)\s*}/g // {@link someSymbol} + const link2 = /\[([^\]]+?)\]{@link(code|plain)?\s+([^\s}|]+?)\s*}/g // [caption here]{@link someSymbol} + const link3 = /{@link(code|plain)?\s+([^\s}|]+?)\s*\|([^}]+?)}/g // {@link someSymbol|caption here} + const link4 = /{@link(code|plain)?\s+([^\s}|]+?)\s+([^}|]+?)}/g // {@link someSymbol Caption Here} while ((matches = link4.exec(text)) !== null) { results.push({ original: matches[0], - caption: matches[2], - url: matches[1] + caption: matches[3], + url: matches[2], + format: matches[1] }) text = text.replace(matches[0], ' '.repeat(matches[0].length)) } @@ -644,8 +647,9 @@ function parseLink (text) { while ((matches = link3.exec(text)) !== null) { results.push({ original: matches[0], - caption: matches[2], - url: matches[1] + caption: matches[3], + url: matches[2], + format: matches[1] }) text = text.replace(matches[0], ' '.repeat(matches[0].length)) } @@ -654,7 +658,8 @@ function parseLink (text) { results.push({ original: matches[0], caption: matches[1], - url: matches[2] + url: matches[3], + format: matches[2] }) text = text.replace(matches[0], ' '.repeat(matches[0].length)) } @@ -662,11 +667,26 @@ function parseLink (text) { while ((matches = link1.exec(text)) !== null) { results.push({ original: matches[0], - caption: matches[1], - url: matches[1] + caption: matches[2], + url: matches[2], + format: matches[1] }) text = text.replace(matches[0], ' '.repeat(matches[0].length)) } + + results.forEach((result) => { + const format = result.format + if (format === undefined) { + result.format = format // if tag is @linkplain or @linkcode, then that determines the format + // else, if 'clever-links' is true, then if the link is a URL, it's plain, otherwise code format + || (dmdOptions['clever-links'] && (urlRe.test(result.url) ? 'plain' : 'code')) + // else, if 'monospace-links' is true, then all links are code format + || (dmdOptions['monospace-links'] && 'code') + // else, it's a plain + || 'plain' + } + }) + return results } diff --git a/helpers/helpers.js b/helpers/helpers.js index e53ce1e..308ca63 100644 --- a/helpers/helpers.js +++ b/helpers/helpers.js @@ -40,16 +40,18 @@ function escape (input) { } /** -replaces {@link} tags with markdown links in the suppied input text +replaces {@link}, {@linkplain}, and {@linkcode} tags with markdown links in the supplied input text */ function inlineLinks (text, options) { if (text) { - const links = ddata.parseLink(text) + const dmdOptions = options.data.root.options + const links = ddata.parseLink(text, dmdOptions) links.forEach(function (link) { + const captionFmt = link.format === 'code' ? '`' : '' const linked = ddata._link(link.url, options) if (link.caption === link.url) link.caption = linked.name if (linked.url) link.url = linked.url - text = text.replace(link.original, '[' + link.caption + '](' + link.url + ')') + text = text.replace(link.original, '[' + captionFmt + link.caption + captionFmt + '](' + link.url + ')') }) } return text diff --git a/lib/dmd-options.js b/lib/dmd-options.js index 5dbe8fc..df839cf 100644 --- a/lib/dmd-options.js +++ b/lib/dmd-options.js @@ -80,6 +80,20 @@ function DmdOptions (options) { */ this['member-index-format'] = 'grouped' + /** + * If true, \{@link XXX} tags are rendered in normal text if XXX is a URL and monospace (code) format otherwise. + * @type {boolean} + * @dafult + */ + this['clever-links'] = false + + /** + * If true, all \{@link} tags are rendered in monospace (code) format. This setting is ignored in `clever-links` is true. + * @type {boolean} + * @default + */ + this['monospace-links'] = false + /** * Show identifiers marked `@private` in the output. * @type {boolean} diff --git a/package-lock.json b/package-lock.json index 826348d..ad58b5b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,6 +19,7 @@ "reduce-flatten": "^3.0.1", "reduce-unique": "^2.0.1", "reduce-without": "^1.0.1", + "regex-repo": "^5.0.0", "test-value": "^3.0.0", "walk-back": "^5.1.1" }, @@ -452,6 +453,15 @@ "node": ">=0.10.0" } }, + "node_modules/regex-repo": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/regex-repo/-/regex-repo-5.0.0.tgz", + "integrity": "sha512-6sVOLr5uq9AxMiRh9Eu9ip66kk4viqnOk8ZYqWihDmXIRZqLqEv1DKyDLwVyOCwaErjhXmBKvAYwtQ9cC2GDSQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -622,9 +632,9 @@ "license": "MIT" }, "node_modules/uglify-js": { - "version": "3.19.2", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.2.tgz", - "integrity": "sha512-S8KA6DDI47nQXJSi2ctQ629YzwOVs+bQML6DAtvy0wgNdpi+0ySpQK0g2pxBq2xfF2z3YCscu7NNA8nXT9PlIQ==", + "version": "3.19.3", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", + "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", "license": "BSD-2-Clause", "optional": true, "bin": { diff --git a/package.json b/package.json index 7156ffa..cf33262 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "reduce-flatten": "^3.0.1", "reduce-unique": "^2.0.1", "reduce-without": "^1.0.1", + "regex-repo": "^5.0.0", "test-value": "^3.0.0", "walk-back": "^5.1.1" }, diff --git a/test/ddata/parseLink.js b/test/ddata/parseLink.js index 64b2cfb..840223f 100644 --- a/test/ddata/parseLink.js +++ b/test/ddata/parseLink.js @@ -5,7 +5,19 @@ const [test, only, skip] = [new Map(), new Map(), new Map()] test.set('{@link someSymbol}', function () { const text = 'blah {@link someSymbol}' - const result = [{ original: '{@link someSymbol}', caption: 'someSymbol', url: 'someSymbol' }] + const result = [{ original: '{@link someSymbol}', caption: 'someSymbol', url: 'someSymbol', format: 'plain' }] + a.deepEqual(ddata.parseLink(text), result) +}) + +test.set('{@linkcode someSymbol}', function () { + const text = 'blah {@linkcode someSymbol}' + const result = [{ original: '{@linkcode someSymbol}', caption: 'someSymbol', url: 'someSymbol', format: 'code' }] + a.deepEqual(ddata.parseLink(text), result) +}) + +test.set('{@linkplain someSymbol}', function () { + const text = 'blah {@linkplain someSymbol}' + const result = [{ original: '{@linkplain someSymbol}', caption: 'someSymbol', url: 'someSymbol', format: 'plain' }] a.deepEqual(ddata.parseLink(text), result) }) @@ -14,7 +26,30 @@ test.set('{@link http://some.url.com}', function () { const result = [{ original: '{@link http://some.url.com}', caption: 'http://some.url.com', - url: 'http://some.url.com' + url: 'http://some.url.com', + format: 'plain' + }] + a.deepEqual(ddata.parseLink(text), result) +}) + +test.set('{@linkcode http://some.url.com}', function () { + const text = 'blah {@linkcode http://some.url.com} blah' + const result = [{ + original: '{@linkcode http://some.url.com}', + caption: 'http://some.url.com', + url: 'http://some.url.com', + format: 'code' + }] + a.deepEqual(ddata.parseLink(text), result) +}) + +test.set('{@linkplain http://some.url.com}', function () { + const text = 'blah {@linkplain http://some.url.com} blah' + const result = [{ + original: '{@linkplain http://some.url.com}', + caption: 'http://some.url.com', + url: 'http://some.url.com', + format: 'plain' }] a.deepEqual(ddata.parseLink(text), result) }) @@ -25,12 +60,14 @@ test.set('multiple {@link http://some.url.com}', function () { { original: '{@link http://one.url.com}', caption: 'http://one.url.com', - url: 'http://one.url.com' + url: 'http://one.url.com', + format: 'plain' }, { original: '{@link http://two.url.com}', caption: 'http://two.url.com', - url: 'http://two.url.com' + url: 'http://two.url.com', + format: 'plain' } ] a.deepEqual(ddata.parseLink(text), expected) @@ -41,7 +78,30 @@ test.set('[caption here]{@link someSymbol}', function () { const result = [{ original: '[caption here]{@link someSymbol}', caption: 'caption here', - url: 'someSymbol' + url: 'someSymbol', + format: 'plain' + }] + a.deepEqual(ddata.parseLink(text), result) +}) + +test.set('[caption here]{@linkcode someSymbol}', function () { + const text = 'blah [caption here]{@linkcode someSymbol} blah' + const result = [{ + original: '[caption here]{@linkcode someSymbol}', + caption: 'caption here', + url: 'someSymbol', + format: 'code' + }] + a.deepEqual(ddata.parseLink(text), result) +}) + +test.set('[caption here]{@linkplain someSymbol}', function () { + const text = 'blah [caption here]{@linkplain someSymbol} blah' + const result = [{ + original: '[caption here]{@linkplain someSymbol}', + caption: 'caption here', + url: 'someSymbol', + format: 'plain' }] a.deepEqual(ddata.parseLink(text), result) }) @@ -52,12 +112,14 @@ test.set('multiple [caption here]{@link someSymbol}', function () { { original: '[caption one]{@link thingOne}', caption: 'caption one', - url: 'thingOne' + url: 'thingOne', + format: 'plain' }, { original: '[caption two]{@link thingTwo}', caption: 'caption two', - url: 'thingTwo' + url: 'thingTwo', + format: 'plain' } ] a.deepEqual(ddata.parseLink(text), result) @@ -68,7 +130,8 @@ test.set('[caption here]{@link http://some.url.com}', function () { const result = [{ original: '[caption here]{@link http://some.url.com}', caption: 'caption here', - url: 'http://some.url.com' + url: 'http://some.url.com', + format: 'plain' }] a.deepEqual(ddata.parseLink(text), result) }) @@ -79,12 +142,33 @@ test.set('multiple {@link someSymbol|caption here}', function () { { original: '{@link thingOne|caption one}', caption: 'caption one', - url: 'thingOne' + url: 'thingOne', + format: 'plain' }, { original: '{@link thingTwo|caption two}', caption: 'caption two', - url: 'thingTwo' + url: 'thingTwo', + format: 'plain' + } + ] + a.deepEqual(ddata.parseLink(text), result) +}) + +test.set('mixed {@link(plain/code) someSymbol|caption here}', function () { + const text = 'blah {@linkplain thingOne|caption one} blah {@linkcode thingTwo|caption two} whatever' + const result = [ + { + original: '{@linkplain thingOne|caption one}', + caption: 'caption one', + url: 'thingOne', + format: 'plain' + }, + { + original: '{@linkcode thingTwo|caption two}', + caption: 'caption two', + url: 'thingTwo', + format: 'code' } ] a.deepEqual(ddata.parseLink(text), result) @@ -96,15 +180,163 @@ test.set('multiple {@link someSymbol Caption here}', function () { { original: '{@link thingOne Caption one}', caption: 'Caption one', - url: 'thingOne' + url: 'thingOne', + format: 'plain' }, { original: '{@link thingTwo Caption two}', caption: 'Caption two', - url: 'thingTwo' + url: 'thingTwo', + format: 'plain' + } + ] + a.deepEqual(ddata.parseLink(text), result) +}) + +test.set('multiple {@link(plain/code) someSymbol Caption here}', function () { + const text = 'blah {@linkplain thingOne Caption one} blah {@linkcode thingTwo Caption two} whatever' + const result = [ + { + original: '{@linkplain thingOne Caption one}', + caption: 'Caption one', + url: 'thingOne', + format: 'plain' + }, + { + original: '{@linkcode thingTwo Caption two}', + caption: 'Caption two', + url: 'thingTwo', + format: 'code' } ] a.deepEqual(ddata.parseLink(text), result) }) +// {@link symbol catption} style +const allLinksText = 'blah {@linkplain thingOne Caption one} blah {@linkcode ftp://url-two.tld Caption two} whatever {@link thingThree Caption three} !@ {@link https://url-four.com Caption four} ok ' + // {@link symbol|caption} style + + '{@linkplain thingFive|caption five} nah {@linkcode git://url-six.com|caption six} ??? {@link thingSeven|caption seven} {@link https://url-eight.net|caption eight} @typedef ' + // [caption]{@link symbol} style + + '[caption nine]{@linkplain symbolNine} ach [caption ten]{@linkcode http://url.ten.com} 2434 [caption eleven]{@link symbolEleven} http://foo.com [caption twelve]{@link http://url.12.com} whawha' + // {@link symbol} style + + '{@linkplain symbolThirteen} fee {@linkcode proto://fourteen.asbf} blb {@link symbolFifteen} geez {@link telnet://16.123.123.123}' + +const cleverLinksResults = [ + { + original: '{@linkplain thingOne Caption one}', + caption: 'Caption one', + url: 'thingOne', + format: 'plain' + }, + { + original: '{@linkcode ftp://url-two.tld Caption two}', + caption: 'Caption two', + url: 'ftp://url-two.tld', + format: 'code' + }, + { + original: '{@link thingThree Caption three}', + caption: 'Caption three', + url: 'thingThree', + format: 'code', + }, + { + original: '{@link https://url-four.com Caption four}', + caption: 'Caption four', + url: 'https://url-four.com', + format: 'plain' + }, + { + original: '{@linkplain thingFive|caption five}', + caption: 'caption five', + url: 'thingFive', + format: 'plain' + }, + { + original: '{@linkcode git://url-six.com|caption six}', + caption: 'caption six', + url: 'git://url-six.com', + format: 'code' + }, + { + original: '{@link thingSeven|caption seven}', + caption: 'caption seven', + url: 'thingSeven', + format: 'code', + }, + { + original: '{@link https://url-eight.net|caption eight}', + caption: 'caption eight', + url: 'https://url-eight.net', + format: 'plain' + }, + { + original: '[caption nine]{@linkplain symbolNine}', + caption: 'caption nine', + url: 'symbolNine', + format: 'plain' + }, + { + original: '[caption ten]{@linkcode http://url.ten.com}', + caption: 'caption ten', + url: 'http://url.ten.com', + format: 'code' + }, + { + original: '[caption eleven]{@link symbolEleven}', + caption: 'caption eleven', + url: 'symbolEleven', + format: 'code' + }, + { + original: '[caption twelve]{@link http://url.12.com}', + caption: 'caption twelve', + url: 'http://url.12.com', + format: 'plain' + }, + { + original: '{@linkplain symbolThirteen}', + caption: 'symbolThirteen', + url: 'symbolThirteen', + format: 'plain', + }, + { + original: '{@linkcode proto://fourteen.asbf}', + caption: 'proto://fourteen.asbf', + url: 'proto://fourteen.asbf', + format: 'code' + }, + { + original: '{@link symbolFifteen}', + caption: 'symbolFifteen', + url: 'symbolFifteen', + format: 'code' + }, + { + original: '{@link telnet://16.123.123.123}', + caption: 'telnet://16.123.123.123', + url: 'telnet://16.123.123.123', + format: 'plain' + } +] + +test.set("'clever-links' true, 'monospace-links' undefined", function() { + a.deepEqual(ddata.parseLink(allLinksText, { 'clever-links': true }), cleverLinksResults) +}) + +test.set("'clever-links' true overrides 'monospace-links' true", function() { + a.deepEqual(ddata.parseLink(allLinksText, { 'clever-links': true, 'monospace-links': true }), cleverLinksResults) +}) + +test.set("'monospace-links' set all {@link}s to 'code' format", function() { + const monospaceLinkResults = cleverLinksResults.map((result) => { + const newResult = Object.assign({}, result) + if (!/@link(?:code|plain)/.test(result.original)) { + newResult.format = 'code' + } + return newResult + }) + a.deepEqual(ddata.parseLink(allLinksText, { 'monospace-links': true }), monospaceLinkResults) +}) + module.exports = { test, only, skip }