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,8 +21,60 @@
// ***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) { /**
*
* @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;
captureStartEvent
.setOutput(null)
._setGlobals(globals)
._setOptions(options)
.setRegexp(pattern)
.setMatches({
_wholeMatch: wholeMatch,
heading: headingText
})
.setAttributes({
id: headingId
});
captureStartEvent = globals.converter.dispatch(captureStartEvent);
// if something was passed as output, it takes precedence
// and will be used as output
if (captureStartEvent.output && captureStartEvent.output !== '') {
otp = captureStartEvent.output;
} else {
headingText = captureStartEvent.matches.heading;
let spanGamut = showdown.subParser('makehtml.spanGamut')(headingText, options, globals),
attributes = captureStartEvent.attributes;
otp = '<h' + headingLevel + showdown.helper._populateAttributes(attributes) + '>' + spanGamut + '</h' + headingLevel + '>';
}
let beforeHashEvent = new showdown.Event('makehtml.heading.onHash', otp);
beforeHashEvent
.setOutput(otp)
._setGlobals(globals)
._setOptions(options);
beforeHashEvent = globals.converter.dispatch(beforeHashEvent);
otp = beforeHashEvent.output;
return showdown.subParser('makehtml.hashBlock')(otp, options, globals);
}
showdown.subParser('makehtml.heading', function (text, options, globals) {
'use strict'; 'use strict';
let startEvent = new showdown.Event('makehtml.heading.onStart', text); let startEvent = new showdown.Event('makehtml.heading.onStart', text);
@ -33,24 +85,8 @@ showdown.subParser('makehtml.heading', function (text, options, globals) {
startEvent = globals.converter.dispatch(startEvent); startEvent = globals.converter.dispatch(startEvent);
text = startEvent.output; text = startEvent.output;
let setextRegexH1 = /^( {0,3}([^ \t\n]+.*\n)(.+\n)?(.+\n)?)( {0,3}=+[ \t]*)$/gm, text = showdown.subParser('makehtml.heading.setext')(text, options, globals);
setextRegexH2 = /^( {0,3}([^ \t\n]+.*\n)(.+\n)?(.+\n)?)( {0,3}(-+)[ \t]*)$/gm, text = showdown.subParser('makehtml.heading.atx')(text, options, globals);
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); let afterEvent = new showdown.Event('makehtml.heading.onEnd', text);
afterEvent afterEvent
@ -60,6 +96,103 @@ showdown.subParser('makehtml.heading', function (text, options, globals) {
afterEvent = globals.converter.dispatch(afterEvent); afterEvent = globals.converter.dispatch(afterEvent);
return afterEvent.output; 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;
// 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.ghCompatibleHeaderId) {
title = title
.replace(/ /g, '-')
// replace previously escaped chars (&, ¨ and $)
.replace(/&amp;/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(/&amp;/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 (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) { function parseSetextHeading (pattern, headingLevel, wholeMatch, headingText, line1, line2, line3, line4) {
@ -163,16 +296,17 @@ showdown.subParser('makehtml.heading', function (text, options, globals) {
// all edge cases should be treated now // all edge cases should be treated now
multilineText = line1 + line2 + ((line3) ? line3 : ''); multilineText = line1 + line2 + ((line3) ? line3 : '');
if (line4.trim().match(/^=+/)) { //if (line4.trim().match(/^=+/)) {
multilineText += line4; // multilineText += line4;
} //}
nPrepend = showdown.subParser('makehtml.blockGamut')(multilineText, options, globals, 'makehtml.heading'); nPrepend = showdown.subParser('makehtml.blockGamut')(multilineText, options, globals, 'makehtml.heading.setext');
if (nPrepend !== multilineText) { if (nPrepend !== multilineText) {
// we found a block, so it should take precedence // we found a block, so it should take precedence
prepend += nPrepend; prepend += nPrepend;
headingText = ''; headingText = '';
} }
console.log(prepend);
} }
// trim stuff // trim stuff
@ -186,113 +320,39 @@ showdown.subParser('makehtml.heading', function (text, options, globals) {
// after this, we're pretty sure it's a heading so let's proceed // 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); let id = (options.noHeaderId) ? null : showdown.subParser('makehtml.heading.id')(headingText, options, globals);
return prepend + parseHeader(pattern, wholeMatch, headingText, headingLevel, id); return prepend + parseHeader(pattern, wholeMatch, headingText, headingLevel, id, options, globals);
} }
function parseHeader (pattern, wholeMatch, headingText, headingLevel, headingId) {
let captureStartEvent = new showdown.Event('makehtml.heading.onCapture', headingText),
otp;
captureStartEvent
.setOutput(null)
._setGlobals(globals)
._setOptions(options)
.setRegexp(pattern)
.setMatches({
_wholeMatch: wholeMatch,
heading: headingText
})
.setAttributes({
id: headingId
}); });
captureStartEvent = globals.converter.dispatch(captureStartEvent);
// if something was passed as output, it takes precedence
// and will be used as output
if (captureStartEvent.output && captureStartEvent.output !== '') {
otp = captureStartEvent.output;
} else { showdown.subParser('makehtml.heading.atx', function (text, options, globals) {
headingText = captureStartEvent.matches.heading;
let spanGamut = showdown.subParser('makehtml.spanGamut')(headingText, options, globals),
attributes = captureStartEvent.attributes;
otp = '<h' + headingLevel + showdown.helper._populateAttributes(attributes) + '>' + spanGamut + '</h' + headingLevel + '>';
}
let beforeHashEvent = new showdown.Event('makehtml.heading.onHash', otp); let startEvent = new showdown.Event('makehtml.heading.atx.onStart', text);
beforeHashEvent startEvent
.setOutput(otp) .setOutput(text)
._setGlobals(globals) ._setGlobals(globals)
._setOptions(options); ._setOptions(options);
beforeHashEvent = globals.converter.dispatch(beforeHashEvent); startEvent = globals.converter.dispatch(startEvent);
otp = beforeHashEvent.output; text = startEvent.output;
return showdown.subParser('makehtml.hashBlock')(otp, options, globals); 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);
showdown.subParser('makehtml.heading.id', function (m, options, globals) { return showdown.subParser('makehtml.hashHTMLBlocks')(afterEvent.output, 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; })();
// 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.ghCompatibleHeaderId) {
title = title
.replace(/ /g, '-')
// replace previously escaped chars (&, ¨ and $)
.replace(/&amp;/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(/&amp;/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 (globals.hashLinkCounts[title]) {
title = title + '-' + (globals.hashLinkCounts[title]++);
} else {
globals.hashLinkCounts[title] = 1;
}
return title;
});

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;