headings compliance

This commit is contained in:
Estevão Soares dos Santos 2022-11-15 02:52:24 +00:00
parent 75b7707460
commit 42959b78c1
5 changed files with 314 additions and 240 deletions

View File

@ -23,10 +23,11 @@ showdown.subParser('makehtml.blockGamut', function (text, options, globals, skip
startEvent = globals.converter.dispatch(startEvent); startEvent = globals.converter.dispatch(startEvent);
text = startEvent.output; text = startEvent.output;
// we parse blockquotes first so that we can have headings and hrs if (skip !== 'makehtml.heading.setext') {
// inside blockquotes text = showdown.subParser('makehtml.heading.setext')(text, options, globals);
if (skip !== 'makehtml.heading') { }
text = showdown.subParser('makehtml.heading')(text, options, globals); if (skip !== 'makehtml.heading.atx') {
text = showdown.subParser('makehtml.heading.atx')(text, options, globals);
} }
// Do Horizontal Rules: // Do Horizontal Rules:

View File

@ -90,8 +90,7 @@ showdown.subParser('makehtml.codeSpan', function (text, options, globals) {
beforeHashEvent = globals.converter.dispatch(beforeHashEvent); beforeHashEvent = globals.converter.dispatch(beforeHashEvent);
otp = beforeHashEvent.output; otp = beforeHashEvent.output;
return showdown.subParser('makehtml.hashHTMLSpans')(otp, options, globals); return showdown.subParser('makehtml.hashHTMLSpans')(otp, options, globals);
} });
);
let afterEvent = new showdown.Event('makehtml.codeSpan.onEnd', text); let afterEvent = new showdown.Event('makehtml.codeSpan.onEnd', text);
afterEvent afterEvent

View File

@ -21,175 +21,20 @@
// ***Author:*** // ***Author:***
// - Estêvão Soares dos Santos (Tivie) <https://github.com/tivie> // - Estêvão Soares dos Santos (Tivie) <https://github.com/tivie>
//// ////
(function () {
showdown.subParser('makehtml.heading', function (text, options, globals) { /**
'use strict'; *
* @param {RegExp} pattern
let startEvent = new showdown.Event('makehtml.heading.onStart', text); * @param {string} wholeMatch
startEvent * @param {string} headingText
.setOutput(text) * @param {string} headingLevel
._setGlobals(globals) * @param {string} headingId
._setOptions(options); * @param {{}} options
startEvent = globals.converter.dispatch(startEvent); * @param {{}} globals
text = startEvent.output; * @returns {string}
*/
let setextRegexH1 = /^( {0,3}([^ \t\n]+.*\n)(.+\n)?(.+\n)?)( {0,3}=+[ \t]*)$/gm, function parseHeader (pattern, wholeMatch, headingText, headingLevel, headingId, options, globals) {
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) {
let captureStartEvent = new showdown.Event('makehtml.heading.onCapture', headingText), let captureStartEvent = new showdown.Event('makehtml.heading.onCapture', headingText),
otp; otp;
@ -229,70 +74,285 @@ showdown.subParser('makehtml.heading', function (text, options, globals) {
return showdown.subParser('makehtml.hashBlock')(otp, 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 startEvent = new showdown.Event('makehtml.heading.onStart', text);
let title, startEvent
prefix; .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 text = showdown.subParser('makehtml.heading.setext')(text, options, globals);
if (options.customizedHeaderId) { text = showdown.subParser('makehtml.heading.atx')(text, options, globals);
let match = m.match(/{([^{]+?)}\s*$/);
if (match && match[1]) { let afterEvent = new showdown.Event('makehtml.heading.onEnd', text);
m = match[1]; 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. // Prefix id to prevent causing inadvertent pre-existing style matches.
if (showdown.helper.isString(options.prefixHeaderId)) { if (showdown.helper.isString(options.prefixHeaderId)) {
prefix = options.prefixHeaderId; prefix = options.prefixHeaderId;
} else if (options.prefixHeaderId === true) { } else if (options.prefixHeaderId === true) {
prefix = 'section-'; prefix = 'section-';
} else { } else {
prefix = ''; prefix = '';
} }
if (!options.rawPrefixHeaderId) { if (!options.rawPrefixHeaderId) {
title = prefix + title; title = prefix + title;
} }
if (options.ghCompatibleHeaderId) { if (options.ghCompatibleHeaderId) {
title = title title = title
.replace(/ /g, '-') .replace(/ /g, '-')
// replace previously escaped chars (&, ¨ and $) // replace previously escaped chars (&, ¨ and $)
.replace(/&amp;/g, '') .replace(/&amp;/g, '')
.replace(/¨T/g, '') .replace(/¨T/g, '')
.replace(/¨D/g, '') .replace(/¨D/g, '')
// replace rest of the chars (&~$ are repeated as they might have been escaped) // replace rest of the chars (&~$ are repeated as they might have been escaped)
// borrowed from github's redcarpet (so they should produce similar results) // borrowed from github's redcarpet (so they should produce similar results)
.replace(/[&+$,\/:;=?@"#{}|^¨~\[\]`\\*)(%.!'<>]/g, '') .replace(/[&+$,\/:;=?@"#{}|^¨~\[\]`\\*)(%.!'<>]/g, '')
.toLowerCase(); .toLowerCase();
} else if (options.rawHeaderId) { } else if (options.rawHeaderId) {
title = title title = title
.replace(/ /g, '-') .replace(/ /g, '-')
// replace previously escaped chars (&, ¨ and $) // replace previously escaped chars (&, ¨ and $)
.replace(/&amp;/g, '&') .replace(/&amp;/g, '&')
.replace(/¨T/g, '¨') .replace(/¨T/g, '¨')
.replace(/¨D/g, '$') .replace(/¨D/g, '$')
// replace " and ' // replace " and '
.replace(/["']/g, '-') .replace(/["']/g, '-')
.toLowerCase(); .toLowerCase();
} else { } else {
title = title title = title
.replace(/\W/g, '') .replace(/\W/g, '')
.toLowerCase(); .toLowerCase();
} }
if (options.rawPrefixHeaderId) { if (options.rawPrefixHeaderId) {
title = prefix + title; title = prefix + title;
} }
if (globals.hashLinkCounts[title]) { if (globals.hashLinkCounts[title]) {
title = title + '-' + (globals.hashLinkCounts[title]++); title = title + '-' + (globals.hashLinkCounts[title]++);
} else { } else {
globals.hashLinkCounts[title] = 1; globals.hashLinkCounts[title] = 1;
} }
return title; 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);
});
})();

View File

@ -1,7 +1,3 @@
<p>this is some text</p> <p>this is some text</p>
<p><code>php <p><code>php function thisThing() { echo "some weird formatted code!"; } </code></p>
function thisThing() {
echo "some weird formatted code!";
}
</code></p>
<p>some other text</p> <p>some other text</p>

View File

@ -109,16 +109,32 @@ describe('showdown.Event', function () {
{ event: 'onHash', text: '```\nfoo\n```', result: true }, { event: 'onHash', text: '```\nfoo\n```', result: true },
{ event: 'onHash', text: 'foo', result: false } { event: 'onHash', text: 'foo', result: false }
], ],
heading: [ 'heading.atx': [
{ event: 'onStart', text: '# foo', result: true }, { event: 'onStart', text: '# foo', result: true },
{ 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: 'onEnd', text: 'foo', result: true }, { event: 'onEnd', text: 'foo', result: true },
{ event: 'onCapture', 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\n===', result: true }, { event: 'onCapture', text: 'foo\n===', result: true },
{ event: 'onCapture', text: 'foo', result: false }, { 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\n===', result: true }, { event: 'onHash', text: 'foo\n===', result: true },
{ event: 'onHash', text: 'foo', result: false } { event: 'onHash', text: 'foo', result: false }
@ -267,8 +283,10 @@ describe('showdown.Event', function () {
describe(parser, function () { describe(parser, function () {
for (let ts in testSpec.makehtml[parser]) { for (let ts in testSpec.makehtml[parser]) {
let event = 'makehtml.' + parser + '.' + testSpec.makehtml[parser][ts].event; let event = 'makehtml.' + parser + '.' + testSpec.makehtml[parser][ts].event;
let md = testSpec.makehtml[parser][ts].text; 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"'; title += 'trigger "' + event + ' event"';
let expected = testSpec.makehtml[parser][ts].result; let expected = testSpec.makehtml[parser][ts].result;
let actual = false; let actual = false;