From 42959b78c188a86f445d0ddab976d5e3c92be8d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Estev=C3=A3o=20Soares=20dos=20Santos?= Date: Tue, 15 Nov 2022 02:52:24 +0000 Subject: [PATCH] headings compliance --- src/subParsers/makehtml/blockGamut.js | 9 +- src/subParsers/makehtml/codeSpan.js | 3 +- src/subParsers/makehtml/heading.js | 512 ++++++++++-------- .../cases/features/disable-gh-codeblocks.html | 6 +- test/unit/showdown.events.js | 24 +- 5 files changed, 314 insertions(+), 240 deletions(-) diff --git a/src/subParsers/makehtml/blockGamut.js b/src/subParsers/makehtml/blockGamut.js index e858ad0..c02902f 100644 --- a/src/subParsers/makehtml/blockGamut.js +++ b/src/subParsers/makehtml/blockGamut.js @@ -23,10 +23,11 @@ showdown.subParser('makehtml.blockGamut', function (text, options, globals, skip startEvent = globals.converter.dispatch(startEvent); text = startEvent.output; - // we parse blockquotes first so that we can have headings and hrs - // inside blockquotes - if (skip !== 'makehtml.heading') { - text = showdown.subParser('makehtml.heading')(text, options, globals); + if (skip !== 'makehtml.heading.setext') { + text = showdown.subParser('makehtml.heading.setext')(text, options, globals); + } + if (skip !== 'makehtml.heading.atx') { + text = showdown.subParser('makehtml.heading.atx')(text, options, globals); } // Do Horizontal Rules: diff --git a/src/subParsers/makehtml/codeSpan.js b/src/subParsers/makehtml/codeSpan.js index dff54ab..fb5d364 100644 --- a/src/subParsers/makehtml/codeSpan.js +++ b/src/subParsers/makehtml/codeSpan.js @@ -90,8 +90,7 @@ showdown.subParser('makehtml.codeSpan', function (text, options, globals) { beforeHashEvent = globals.converter.dispatch(beforeHashEvent); otp = beforeHashEvent.output; return showdown.subParser('makehtml.hashHTMLSpans')(otp, options, globals); - } - ); + }); let afterEvent = new showdown.Event('makehtml.codeSpan.onEnd', text); afterEvent diff --git a/src/subParsers/makehtml/heading.js b/src/subParsers/makehtml/heading.js index 9b52d67..d7c76c7 100644 --- a/src/subParsers/makehtml/heading.js +++ b/src/subParsers/makehtml/heading.js @@ -21,175 +21,20 @@ // ***Author:*** // - Estêvão Soares dos Santos (Tivie) //// +(function () { -showdown.subParser('makehtml.heading', function (text, options, globals) { - 'use strict'; - - let startEvent = new showdown.Event('makehtml.heading.onStart', text); - startEvent - .setOutput(text) - ._setGlobals(globals) - ._setOptions(options); - startEvent = globals.converter.dispatch(startEvent); - text = startEvent.output; - - let setextRegexH1 = /^( {0,3}([^ \t\n]+.*\n)(.+\n)?(.+\n)?)( {0,3}=+[ \t]*)$/gm, - setextRegexH2 = /^( {0,3}([^ \t\n]+.*\n)(.+\n)?(.+\n)?)( {0,3}(-+)[ \t]*)$/gm, - atxRegex = (options.requireSpaceBeforeHeadingText) ? /^ {0,3}(#{1,6})[ \t]+(.+?)(?:[ \t]+#+)?[ \t]*$/gm : /^ {0,3}(#{1,6})[ \t]*(.+?)[ \t]*#*[ \t]*$/gm; - - text = text.replace(setextRegexH1, function (wholeMatch, headingText, line1, line2, line3, line4) { - return parseSetextHeading(setextRegexH2, options.headerLevelStart, wholeMatch, headingText, line1, line2, line3, line4); - }); - - text = text.replace(setextRegexH2, function (wholeMatch, headingText, line1, line2, line3, line4) { - return parseSetextHeading(setextRegexH2, options.headerLevelStart + 1, wholeMatch, headingText, line1, line2, line3, line4); - }); - - text = text.replace(atxRegex, function (wholeMatch, m1, m2) { - let headingLevel = options.headerLevelStart - 1 + m1.length, - headingText = (options.customizedHeaderId) ? m2.replace(/\s?{([^{]+?)}\s*$/, '') : m2, - id = (options.noHeaderId) ? null : showdown.subParser('makehtml.heading.id')(m2, options, globals); - return parseHeader(setextRegexH2, wholeMatch, headingText, headingLevel, id); - }); - - let afterEvent = new showdown.Event('makehtml.heading.onEnd', text); - afterEvent - .setOutput(text) - ._setGlobals(globals) - ._setOptions(options); - afterEvent = globals.converter.dispatch(afterEvent); - return afterEvent.output; - - - - function parseSetextHeading (pattern, headingLevel, wholeMatch, headingText, line1, line2, line3, line4) { - - // count lines - let count = headingText.trim().split('\n').length; - let prepend = ''; - let nPrepend; - const hrCheckRgx = /^ {0,3}[-_*]([-_*] ?){2,}$/; - - // one liner edge cases - if (count === 1) { - // hr - // let's find the hr edge case first - if (showdown.helper.trimEnd(line1).match(hrCheckRgx)) { - // it's the edge case, so it's a false positive - prepend = showdown.subParser('makehtml.horizontalRule')(line1, options, globals); - if (prepend !== line1) { - // it's an oneliner list - return prepend.trim() + '\n' + line4; - } - } - - // now check if it's an unordered list - if (line1.match(/^ {0,3}[-*+][ \t]/)) { - if (line4.trim().match(/^=+/)) { - line1 += line4; - } - prepend = showdown.subParser('makehtml.list')(line1, options, globals); - if (prepend !== line1) { - // it's an oneliner list - return prepend.trim() + '\n' + line4; - } - } - - // check if it's a blockquote - if (line1.match(/^ {0,3}>[ \t]?[^ \t]/)) { - if (line4.trim().match(/^=+/)) { - line1 += line4; - } - prepend = showdown.subParser('makehtml.blockquote')(line1, options, globals); - if (prepend !== line1) { - // it's an oneliner blockquote - - return prepend.trim() + '\n' + line4; - } - } - - // no edge case let's proceed as usual - } else { - let multilineText = ''; - - // multiline is a bit trickier - // first we must take care of the edge cases of: - // case1: | case2: - // --- | --- - // foo | foo - // --- | bar - // | --- - // - if (showdown.helper.trimEnd(line1).match(hrCheckRgx)) { - nPrepend = showdown.subParser('makehtml.horizontalRule')(line1, options, globals); - if (nPrepend !== line1) { - line1 = ''; - // we add the parsed block to prepend - prepend = nPrepend.trim(); - // and remove the line from the headingText, so it doesn't appear repeated - headingText = line2 + ((line3) ? line3 : ''); - } - } - - // now we take care of these cases: - // case1: | case2: - // foo | foo - // *** | *** - // --- | bar - // | --- - // - if (showdown.helper.trimEnd(line2).match(hrCheckRgx)) { - // This case sucks, because the first line could be anything!!! - // first let's make sure it's a hr - nPrepend = showdown.subParser('makehtml.horizontalRule')(line2, options, globals); - if (nPrepend !== line2) { - line2 = nPrepend; - // it is, so now we must parse line1 also - if (line1) { - line1 = showdown.subParser('makehtml.blockGamut')(line1, options, globals); - line1 = showdown.subParser('makehtml.paragraphs')(line1, options, globals); - line1 = line1.trim() + '\n'; - prepend = line1; - // and clear line1 - line1 = ''; - } - // we add the parsed blocks to prepend - prepend += line2.trim() + '\n'; - line2 = ''; - // and remove the lines from the headingText, so it doesn't appear repeated - headingText = (line3) ? line3 : ''; - } - } - - // all edge cases should be treated now - multilineText = line1 + line2 + ((line3) ? line3 : ''); - if (line4.trim().match(/^=+/)) { - multilineText += line4; - } - - nPrepend = showdown.subParser('makehtml.blockGamut')(multilineText, options, globals, 'makehtml.heading'); - if (nPrepend !== multilineText) { - // we found a block, so it should take precedence - prepend += nPrepend; - headingText = ''; - } - } - - // trim stuff - headingText = headingText.trim(); - - // let's check if heading is empty - // after looking for blocks, heading text might be empty which is a false positive - if (!headingText) { - return prepend + line4; - } - - // after this, we're pretty sure it's a heading so let's proceed - let id = (options.noHeaderId) ? null : showdown.subParser('makehtml.heading.id')(headingText, options, globals); - return prepend + parseHeader(pattern, wholeMatch, headingText, headingLevel, id); - } - - function parseHeader (pattern, wholeMatch, headingText, headingLevel, headingId) { + /** + * + * @param {RegExp} pattern + * @param {string} wholeMatch + * @param {string} headingText + * @param {string} headingLevel + * @param {string} headingId + * @param {{}} options + * @param {{}} globals + * @returns {string} + */ + function parseHeader (pattern, wholeMatch, headingText, headingLevel, headingId, options, globals) { let captureStartEvent = new showdown.Event('makehtml.heading.onCapture', headingText), otp; @@ -229,70 +74,285 @@ showdown.subParser('makehtml.heading', function (text, options, globals) { return showdown.subParser('makehtml.hashBlock')(otp, options, globals); } -}); + showdown.subParser('makehtml.heading', function (text, options, globals) { + 'use strict'; -showdown.subParser('makehtml.heading.id', function (m, options, globals) { - let title, - prefix; + let startEvent = new showdown.Event('makehtml.heading.onStart', text); + startEvent + .setOutput(text) + ._setGlobals(globals) + ._setOptions(options); + startEvent = globals.converter.dispatch(startEvent); + text = startEvent.output; - // It is separate from other options to allow combining prefix and customized - if (options.customizedHeaderId) { - let match = m.match(/{([^{]+?)}\s*$/); - if (match && match[1]) { - m = match[1]; + text = showdown.subParser('makehtml.heading.setext')(text, options, globals); + text = showdown.subParser('makehtml.heading.atx')(text, options, globals); + + let afterEvent = new showdown.Event('makehtml.heading.onEnd', text); + afterEvent + .setOutput(text) + ._setGlobals(globals) + ._setOptions(options); + afterEvent = globals.converter.dispatch(afterEvent); + return afterEvent.output; + + }); + + showdown.subParser('makehtml.heading.id', function (m, options, globals) { + let title, + prefix; + + // It is separate from other options to allow combining prefix and customized + if (options.customizedHeaderId) { + let match = m.match(/{([^{]+?)}\s*$/); + if (match && match[1]) { + m = match[1]; + } } - } - title = m; + title = m; - // Prefix id to prevent causing inadvertent pre-existing style matches. - if (showdown.helper.isString(options.prefixHeaderId)) { - prefix = options.prefixHeaderId; - } else if (options.prefixHeaderId === true) { - prefix = 'section-'; - } else { - prefix = ''; - } + // Prefix id to prevent causing inadvertent pre-existing style matches. + if (showdown.helper.isString(options.prefixHeaderId)) { + prefix = options.prefixHeaderId; + } else if (options.prefixHeaderId === true) { + prefix = 'section-'; + } else { + prefix = ''; + } - if (!options.rawPrefixHeaderId) { - title = prefix + title; - } + if (!options.rawPrefixHeaderId) { + title = prefix + title; + } - if (options.ghCompatibleHeaderId) { - title = title - .replace(/ /g, '-') - // replace previously escaped chars (&, ¨ and $) - .replace(/&/g, '') - .replace(/¨T/g, '') - .replace(/¨D/g, '') - // replace rest of the chars (&~$ are repeated as they might have been escaped) - // borrowed from github's redcarpet (so they should produce similar results) - .replace(/[&+$,\/:;=?@"#{}|^¨~\[\]`\\*)(%.!'<>]/g, '') - .toLowerCase(); - } else if (options.rawHeaderId) { - title = title - .replace(/ /g, '-') - // replace previously escaped chars (&, ¨ and $) - .replace(/&/g, '&') - .replace(/¨T/g, '¨') - .replace(/¨D/g, '$') - // replace " and ' - .replace(/["']/g, '-') - .toLowerCase(); - } else { - title = title - .replace(/\W/g, '') - .toLowerCase(); - } + if (options.ghCompatibleHeaderId) { + title = title + .replace(/ /g, '-') + // replace previously escaped chars (&, ¨ and $) + .replace(/&/g, '') + .replace(/¨T/g, '') + .replace(/¨D/g, '') + // replace rest of the chars (&~$ are repeated as they might have been escaped) + // borrowed from github's redcarpet (so they should produce similar results) + .replace(/[&+$,\/:;=?@"#{}|^¨~\[\]`\\*)(%.!'<>]/g, '') + .toLowerCase(); + } else if (options.rawHeaderId) { + title = title + .replace(/ /g, '-') + // replace previously escaped chars (&, ¨ and $) + .replace(/&/g, '&') + .replace(/¨T/g, '¨') + .replace(/¨D/g, '$') + // replace " and ' + .replace(/["']/g, '-') + .toLowerCase(); + } else { + title = title + .replace(/\W/g, '') + .toLowerCase(); + } - if (options.rawPrefixHeaderId) { - title = prefix + title; - } + if (options.rawPrefixHeaderId) { + title = prefix + title; + } - if (globals.hashLinkCounts[title]) { - title = title + '-' + (globals.hashLinkCounts[title]++); - } else { - globals.hashLinkCounts[title] = 1; - } - return title; -}); + if (globals.hashLinkCounts[title]) { + title = title + '-' + (globals.hashLinkCounts[title]++); + } else { + globals.hashLinkCounts[title] = 1; + } + return title; + }); + + showdown.subParser('makehtml.heading.setext', function (text, options, globals) { + + let startEvent = new showdown.Event('makehtml.heading.setext.onStart', text); + startEvent + .setOutput(text) + ._setGlobals(globals) + ._setOptions(options); + startEvent = globals.converter.dispatch(startEvent); + text = startEvent.output; + + const setextRegexH1 = /^( {0,3}([^ \t\n]+.*\n)(.+\n)?(.+\n)?)( {0,3}=+[ \t]*)$/gm, + setextRegexH2 = /^( {0,3}([^ \t\n]+.*\n)(.+\n)?(.+\n)?)( {0,3}(-+)[ \t]*)$/gm; + + text = text.replace(setextRegexH1, function (wholeMatch, headingText, line1, line2, line3, line4) { + return parseSetextHeading(setextRegexH2, options.headerLevelStart, wholeMatch, headingText, line1, line2, line3, line4); + }); + + text = text.replace(setextRegexH2, function (wholeMatch, headingText, line1, line2, line3, line4) { + return parseSetextHeading(setextRegexH2, options.headerLevelStart + 1, wholeMatch, headingText, line1, line2, line3, line4); + }); + + let afterEvent = new showdown.Event('makehtml.heading.setext.onEnd', text); + afterEvent + .setOutput(text) + ._setGlobals(globals) + ._setOptions(options); + afterEvent = globals.converter.dispatch(afterEvent); + + return showdown.subParser('makehtml.hashHTMLBlocks')(afterEvent.output, options, globals); + + + function parseSetextHeading (pattern, headingLevel, wholeMatch, headingText, line1, line2, line3, line4) { + + // count lines + let count = headingText.trim().split('\n').length; + let prepend = ''; + let nPrepend; + const hrCheckRgx = /^ {0,3}[-_*]([-_*] ?){2,}$/; + + // one liner edge cases + if (count === 1) { + // hr + // let's find the hr edge case first + if (showdown.helper.trimEnd(line1).match(hrCheckRgx)) { + // it's the edge case, so it's a false positive + prepend = showdown.subParser('makehtml.horizontalRule')(line1, options, globals); + if (prepend !== line1) { + // it's an oneliner list + return prepend.trim() + '\n' + line4; + } + } + + // now check if it's an unordered list + if (line1.match(/^ {0,3}[-*+][ \t]/)) { + if (line4.trim().match(/^=+/)) { + line1 += line4; + } + prepend = showdown.subParser('makehtml.list')(line1, options, globals); + if (prepend !== line1) { + // it's an oneliner list + return prepend.trim() + '\n' + line4; + } + } + + // check if it's a blockquote + if (line1.match(/^ {0,3}>[ \t]?[^ \t]/)) { + if (line4.trim().match(/^=+/)) { + line1 += line4; + } + prepend = showdown.subParser('makehtml.blockquote')(line1, options, globals); + if (prepend !== line1) { + // it's an oneliner blockquote + + return prepend.trim() + '\n' + line4; + } + } + + // no edge case let's proceed as usual + } else { + let multilineText = ''; + + // multiline is a bit trickier + // first we must take care of the edge cases of: + // case1: | case2: + // --- | --- + // foo | foo + // --- | bar + // | --- + // + if (showdown.helper.trimEnd(line1).match(hrCheckRgx)) { + nPrepend = showdown.subParser('makehtml.horizontalRule')(line1, options, globals); + if (nPrepend !== line1) { + line1 = ''; + // we add the parsed block to prepend + prepend = nPrepend.trim(); + // and remove the line from the headingText, so it doesn't appear repeated + headingText = line2 + ((line3) ? line3 : ''); + } + } + + // now we take care of these cases: + // case1: | case2: + // foo | foo + // *** | *** + // --- | bar + // | --- + // + if (showdown.helper.trimEnd(line2).match(hrCheckRgx)) { + // This case sucks, because the first line could be anything!!! + // first let's make sure it's a hr + nPrepend = showdown.subParser('makehtml.horizontalRule')(line2, options, globals); + if (nPrepend !== line2) { + line2 = nPrepend; + // it is, so now we must parse line1 also + if (line1) { + line1 = showdown.subParser('makehtml.blockGamut')(line1, options, globals); + line1 = showdown.subParser('makehtml.paragraphs')(line1, options, globals); + line1 = line1.trim() + '\n'; + prepend = line1; + // and clear line1 + line1 = ''; + } + // we add the parsed blocks to prepend + prepend += line2.trim() + '\n'; + line2 = ''; + // and remove the lines from the headingText, so it doesn't appear repeated + headingText = (line3) ? line3 : ''; + } + } + + // all edge cases should be treated now + multilineText = line1 + line2 + ((line3) ? line3 : ''); + //if (line4.trim().match(/^=+/)) { + // multilineText += line4; + //} + + nPrepend = showdown.subParser('makehtml.blockGamut')(multilineText, options, globals, 'makehtml.heading.setext'); + if (nPrepend !== multilineText) { + // we found a block, so it should take precedence + prepend += nPrepend; + headingText = ''; + } + console.log(prepend); + } + + // trim stuff + headingText = headingText.trim(); + + // let's check if heading is empty + // after looking for blocks, heading text might be empty which is a false positive + if (!headingText) { + return prepend + line4; + } + + // after this, we're pretty sure it's a heading so let's proceed + let id = (options.noHeaderId) ? null : showdown.subParser('makehtml.heading.id')(headingText, options, globals); + return prepend + parseHeader(pattern, wholeMatch, headingText, headingLevel, id, options, globals); + } + + + }); + + showdown.subParser('makehtml.heading.atx', function (text, options, globals) { + + let startEvent = new showdown.Event('makehtml.heading.atx.onStart', text); + startEvent + .setOutput(text) + ._setGlobals(globals) + ._setOptions(options); + startEvent = globals.converter.dispatch(startEvent); + text = startEvent.output; + + const atxRegex = (options.requireSpaceBeforeHeadingText) ? /^ {0,3}(#{1,6})[ \t]+(.+?)(?:[ \t]+#+)?[ \t]*$/gm : /^ {0,3}(#{1,6})[ \t]*(.+?)[ \t]*#*[ \t]*$/gm; + text = text.replace(atxRegex, function (wholeMatch, m1, m2) { + let headingLevel = options.headerLevelStart - 1 + m1.length, + headingText = (options.customizedHeaderId) ? m2.replace(/\s?{([^{]+?)}\s*$/, '') : m2, + id = (options.noHeaderId) ? null : showdown.subParser('makehtml.heading.id')(m2, options, globals); + return parseHeader(atxRegex, wholeMatch, headingText, headingLevel, id, options, globals); + }); + + let afterEvent = new showdown.Event('makehtml.heading.atx.onEnd', text); + afterEvent + .setOutput(text) + ._setGlobals(globals) + ._setOptions(options); + afterEvent = globals.converter.dispatch(afterEvent); + + return showdown.subParser('makehtml.hashHTMLBlocks')(afterEvent.output, options, globals); + + }); + +})(); diff --git a/test/functional/makehtml/cases/features/disable-gh-codeblocks.html b/test/functional/makehtml/cases/features/disable-gh-codeblocks.html index 3d739ef..a252d67 100644 --- a/test/functional/makehtml/cases/features/disable-gh-codeblocks.html +++ b/test/functional/makehtml/cases/features/disable-gh-codeblocks.html @@ -1,7 +1,3 @@

this is some text

-

php -function thisThing() { -echo "some weird formatted code!"; -} -

+

php function thisThing() { echo "some weird formatted code!"; }

some other text

diff --git a/test/unit/showdown.events.js b/test/unit/showdown.events.js index a8a7446..6f5855d 100644 --- a/test/unit/showdown.events.js +++ b/test/unit/showdown.events.js @@ -109,16 +109,32 @@ describe('showdown.Event', function () { { event: 'onHash', text: '```\nfoo\n```', result: true }, { event: 'onHash', text: 'foo', result: false } ], - heading: [ + 'heading.atx': [ { event: 'onStart', text: '# foo', result: true }, { event: 'onStart', text: 'foo', result: true }, { event: 'onEnd', text: '# foo', result: true }, { event: 'onEnd', text: 'foo', result: true }, { event: 'onCapture', text: '# foo', result: true }, + { event: 'onCapture', text: 'foo\n---', result: false }, + { event: 'onCapture', text: 'foo\n===', result: false }, + { event: 'onCapture', text: 'foo', result: false }, + { event: 'onHash', text: '# foo', result: true }, + { event: 'onHash', text: 'foo\n---', result: false }, + { event: 'onHash', text: 'foo\n===', result: false }, + { event: 'onHash', text: 'foo', result: false } + ], + 'heading.setext': [ + { event: 'onStart', text: 'foo\n---', result: true }, + { event: 'onStart', text: 'foo\n===', result: true }, + { event: 'onStart', text: 'foo', result: true }, + { event: 'onEnd', text: 'foo\n---', result: true }, + { event: 'onEnd', text: 'foo\n===', result: true }, + { event: 'onEnd', text: 'foo', result: true }, + { event: 'onCapture', text: '# foo', result: false }, { event: 'onCapture', text: 'foo\n---', result: true }, { event: 'onCapture', text: 'foo\n===', result: true }, { event: 'onCapture', text: 'foo', result: false }, - { event: 'onHash', text: '# foo', result: true }, + { event: 'onHash', text: '# foo', result: false }, { event: 'onHash', text: 'foo\n---', result: true }, { event: 'onHash', text: 'foo\n===', result: true }, { event: 'onHash', text: 'foo', result: false } @@ -267,8 +283,10 @@ describe('showdown.Event', function () { describe(parser, function () { for (let ts in testSpec.makehtml[parser]) { let event = 'makehtml.' + parser + '.' + testSpec.makehtml[parser][ts].event; + let md = testSpec.makehtml[parser][ts].text; - let title = (testSpec.makehtml[parser][ts].result) ? 'should ' : 'should NOT '; + let title = '«' + md + '» '; + title += (testSpec.makehtml[parser][ts].result) ? 'should ' : 'should NOT '; title += 'trigger "' + event + ' event"'; let expected = testSpec.makehtml[parser][ts].result; let actual = false;