diff --git a/.circleci/config.yml b/.circleci/config.yml index 10264d4d3..2025e8fe7 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -70,6 +70,9 @@ jobs: - run: name: Test - Hard Breaks command: npm run test:hard-breaks + - run: + name: Test - Non-Breaking Spaces + command: npm run test:non-breaking-spaces - run: name: Test - Variables command: npm run test:variables diff --git a/package.json b/package.json index aec34b085..2ce79f112 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "test:mustache-syntax:injection": "jest \".*(mustache-syntax).*\" -t '^Injection:.*' --verbose --noStackTrace", "test:definition-lists": "jest tests/markdown/definition-lists.test.js --verbose --noStackTrace", "test:hard-breaks": "jest tests/markdown/hard-breaks.test.js --verbose --noStackTrace", + "test:non-breaking-spaces": "jest tests/markdown/non-breaking-spaces.test.js --verbose --noStackTrace", "test:emojis": "jest tests/markdown/emojis.test.js --verbose --noStackTrace", "test:route": "jest tests/routes/static-pages.test.js --verbose", "test:safehtml": "jest tests/html/safeHTML.test.js --verbose", diff --git a/shared/naturalcrit/markdown.js b/shared/naturalcrit/markdown.js index 4c1a2f92a..852243d81 100644 --- a/shared/naturalcrit/markdown.js +++ b/shared/naturalcrit/markdown.js @@ -391,10 +391,31 @@ const forcedParagraphBreaks = { } }; +const nonbreakingSpaces = { + name : 'nonbreakingSpaces', + level : 'inline', + start(src) { return src.match(/:>+/m)?.index; }, // Hint to Marked.js to stop and check for a match + tokenizer(src, tokens) { + const regex = /:(>+)/ym; + const match = regex.exec(src); + if(match?.length) { + return { + type : 'nonbreakingSpaces', // Should match "name" above + raw : match[0], // Text to consume from the source + length : match[1].length, + text : '' + }; + } + }, + renderer(token) { + return ` `.repeat(token.length).concat(''); + } +}; + const definitionListsSingleLine = { name : 'definitionListsSingleLine', level : 'block', - start(src) { return src.match(/\n[^\n]*?::[^\n]*/m)?.index; }, // Hint to Marked.js to stop and check for a match + start(src) { return src.match(/\n[^\n]*?::[^\n]*/m)?.index; }, // Hint to Marked.js to stop and check for a match tokenizer(src, tokens) { const regex = /^([^\n]*?)::([^\n]*)(?:\n|$)/ym; let match; @@ -748,11 +769,12 @@ const tableTerminators = [ ]; Marked.use(MarkedVariables()); -Marked.use({ extensions : [definitionListsMultiLine, definitionListsSingleLine, forcedParagraphBreaks, superSubScripts, - mustacheSpans, mustacheDivs, mustacheInjectInline] }); +Marked.use({ extensions : [definitionListsMultiLine, definitionListsSingleLine, forcedParagraphBreaks, + nonbreakingSpaces, superSubScripts, mustacheSpans, mustacheDivs, mustacheInjectInline] }); Marked.use(mustacheInjectBlock); Marked.use({ renderer: renderer, tokenizer: tokenizer, mangle: false }); -Marked.use(MarkedExtendedTables(tableTerminators), MarkedGFMHeadingId({ globalSlugs: true }), MarkedSmartypantsLite(), MarkedEmojis(MarkedEmojiOptions)); +Marked.use(MarkedExtendedTables(tableTerminators), MarkedGFMHeadingId({ globalSlugs: true }), + MarkedSmartypantsLite(), MarkedEmojis(MarkedEmojiOptions)); function cleanUrl(href) { try { diff --git a/tests/markdown/non-breaking-spaces.test.js b/tests/markdown/non-breaking-spaces.test.js new file mode 100644 index 000000000..9dad4eb0f --- /dev/null +++ b/tests/markdown/non-breaking-spaces.test.js @@ -0,0 +1,72 @@ +/* eslint-disable max-lines */ + +import Markdown from 'naturalcrit/markdown.js'; + +describe('Non-Breaking Spaces', ()=>{ + test('Single Space', function() { + const source = ':>\n\n'; + const rendered = Markdown.render(source).trim(); + expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`

 

`); + }); + + test('Double Space', function() { + const source = ':>>\n\n'; + const rendered = Markdown.render(source).trim(); + expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`

  

`); + }); + + test('Triple Space', function() { + const source = ':>>>\n\n'; + const rendered = Markdown.render(source).trim(); + expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`

   

`); + }); + + test('Many Space', function() { + const source = ':>>>>>>>>>>\n\n'; + const rendered = Markdown.render(source).trim(); + expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`

          

`); + }); + + test('Multiple sets of Spaces', function() { + const source = ':>>>\n:>>>\n:>>>'; + const rendered = Markdown.render(source).trim(); + expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`

   \n   \n   

`); + }); + + test('Pair of inline Spaces', function() { + const source = ':>>:>>'; + const rendered = Markdown.render(source).trim(); + expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`

    

`); + }); + + test('Space directly between two paragraphs', function() { + const source = 'Line 1\n:>>\nLine 2'; + const rendered = Markdown.render(source).trim(); + expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`

Line 1\n  \nLine 2

`); + }); + + test('Ignored inside a code block', function() { + const source = '```\n\n:>\n\n```\n'; + const rendered = Markdown.render(source).trim(); + expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`
\n:>\n
`); + }); + + test('I am actually a single-line definition list!', function() { + const source = 'Term ::> Definition 1\n'; + const rendered = Markdown.render(source).trim(); + expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`
Term
> Definition 1
\n
`); + }); + + test('I am actually a definition list!', function() { + const source = 'Term\n::> Definition 1\n'; + const rendered = Markdown.render(source).trim(); + expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`
Term
\n
> Definition 1
`); + }); + + test('I am actually a two-term definition list!', function() { + const source = 'Term\n::> Definition 1\n::>> Definition 2'; + const rendered = Markdown.render(source).trim(); + expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`
Term
\n
> Definition 1
\n
>> Definition 2
`); + }); +}); +