diff --git a/.circleci/config.yml b/.circleci/config.yml index 8a756b3de..274ec25ac 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -67,6 +67,9 @@ jobs: - run: name: Test - Definition Lists command: npm run test:definition-lists + - run: + name: Test - Hard Breaks + command: npm run test:hard-breaks - run: name: Test - Variables command: npm run test:variables diff --git a/package-lock.json b/package-lock.json index 08b542f46..9d832091a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -58,7 +58,7 @@ "jest-expect-message": "^1.1.3", "postcss-less": "^6.0.0", "stylelint": "^16.8.2", - "stylelint-config-recess-order": "^5.0.1", + "stylelint-config-recess-order": "^5.1.0", "stylelint-config-recommended": "^14.0.1", "supertest": "^7.0.0" }, @@ -13678,11 +13678,10 @@ } }, "node_modules/stylelint-config-recess-order": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/stylelint-config-recess-order/-/stylelint-config-recess-order-5.0.1.tgz", - "integrity": "sha512-rKbGkoa3h0rINrGln9TFVowvSCLgPJC5O0EuPiqlqWcJMb1lImEtXktcjFCVz+hwtSUiHD3ijJc3vP9muFOgJg==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/stylelint-config-recess-order/-/stylelint-config-recess-order-5.1.0.tgz", + "integrity": "sha512-ddapCF6B/kEtQYIFhQFReQ0dvK1ZdgJDM/SGFtIyeooYDbqaJqcOlGkRRGaVErCQYJY/bPSPsLRS2LdQtLJUVQ==", "dev": true, - "license": "ISC", "dependencies": { "stylelint-order": "^6.0.4" }, diff --git a/package.json b/package.json index 675d76f39..26f68224c 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "test:mustache-syntax:block": "jest \".*(mustache-syntax).*\" -t '^Block:.*' --verbose --noStackTrace", "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:emojis": "jest tests/markdown/emojis.test.js --verbose --noStackTrace", "test:route": "jest tests/routes/static-pages.test.js --verbose", "phb": "node scripts/phb.js", @@ -131,7 +132,7 @@ "jest-expect-message": "^1.1.3", "postcss-less": "^6.0.0", "stylelint": "^16.8.2", - "stylelint-config-recess-order": "^5.0.1", + "stylelint-config-recess-order": "^5.1.0", "stylelint-config-recommended": "^14.0.1", "supertest": "^7.0.0" } diff --git a/server/app.js b/server/app.js index b419c5cea..8b3e4652b 100644 --- a/server/app.js +++ b/server/app.js @@ -14,6 +14,7 @@ const GoogleActions = require('./googleActions.js'); const serveCompressedStaticAssets = require('./static-assets.mv.js'); const sanitizeFilename = require('sanitize-filename'); const asyncHandler = require('express-async-handler'); +const templateFn = require('./../client/template.js'); const { DEFAULT_BREW } = require('./brewDefaults.js'); @@ -420,8 +421,16 @@ if(isLocalEnvironment){ }); } +//Send rendered page +app.use(asyncHandler(async (req, res, next)=>{ + if (!req.route) return res.redirect('/'); // Catch-all for invalid routes + + const page = await renderPage(req, res); + if(!page) return; + res.send(page); +})); + //Render the page -const templateFn = require('./../client/template.js'); const renderPage = async (req, res)=>{ // Create configuration object const configuration = { @@ -450,13 +459,6 @@ const renderPage = async (req, res)=>{ return page; }; -//Send rendered page -app.use(asyncHandler(async (req, res, next)=>{ - const page = await renderPage(req, res); - if(!page) return; - res.send(page); -})); - //v=====----- Error-Handling Middleware -----=====v// //Format Errors as plain objects so all fields will appear in the string sent const formatErrors = (key, value)=>{ diff --git a/shared/naturalcrit/markdown.js b/shared/naturalcrit/markdown.js index 3be8108e4..370349fbc 100644 --- a/shared/naturalcrit/markdown.js +++ b/shared/naturalcrit/markdown.js @@ -370,6 +370,27 @@ const superSubScripts = { } }; +const forcedParagraphBreaks = { + name : 'hardBreaks', + level : 'block', + start(src) { return src.match(/\n:+$/m)?.index; }, // Hint to Marked.js to stop and check for a match + tokenizer(src, tokens) { + const regex = /^(:+)(?:\n|$)/ym; + const match = regex.exec(src); + if(match?.length) { + return { + type : 'hardBreaks', // 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('\n'); + } +}; + const definitionListsSingleLine = { name : 'definitionListsSingleLine', level : 'block', @@ -414,9 +435,9 @@ const definitionListsSingleLine = { const definitionListsMultiLine = { name : 'definitionListsMultiLine', 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::[^:\n]/m)?.index; }, // Hint to Marked.js to stop and check for a match tokenizer(src, tokens) { - const regex = /(\n?\n?(?!::)[^\n]+?(?=\n::))|\n::(.(?:.|\n)*?(?=(?:\n::)|(?:\n\n)|$))/y; + const regex = /(\n?\n?(?!::)[^\n]+?(?=\n::[^:\n]))|\n::([^:\n](?:.|\n)*?(?=(?:\n::)|(?:\n\n)|$))/y; let match; let endIndex = 0; const definitions = []; @@ -721,7 +742,8 @@ const MarkedEmojiOptions = { }; Marked.use(MarkedVariables()); -Marked.use({ extensions: [definitionListsMultiLine, definitionListsSingleLine, superSubScripts, mustacheSpans, mustacheDivs, mustacheInjectInline] }); +Marked.use({ extensions : [definitionListsMultiLine, definitionListsSingleLine, forcedParagraphBreaks, superSubScripts, + mustacheSpans, mustacheDivs, mustacheInjectInline] }); Marked.use(mustacheInjectBlock); Marked.use({ renderer: renderer, tokenizer: tokenizer, mangle: false }); Marked.use(MarkedExtendedTables(), MarkedGFMHeadingId(), MarkedSmartypantsLite(), MarkedEmojis(MarkedEmojiOptions)); @@ -833,8 +855,7 @@ module.exports = { varsQueue = []; //Could move into MarkedVariables() globalPageNumber = pageNumber; - rawBrewText = rawBrewText.replace(/^\\column$/gm, `\n\n`) - .replace(/^(:+)$/gm, (match)=>`${``.repeat(match.length)}\n`); + rawBrewText = rawBrewText.replace(/^\\column$/gm, `\n\n`); const opts = Marked.defaults; rawBrewText = opts.hooks.preprocess(rawBrewText); diff --git a/tests/markdown/definition-lists.test.js b/tests/markdown/definition-lists.test.js index 9f5025d73..9c0bdf6b0 100644 --- a/tests/markdown/definition-lists.test.js +++ b/tests/markdown/definition-lists.test.js @@ -88,4 +88,16 @@ describe('Multiline Definition Lists', ()=>{ const rendered = Markdown.render(source).trim(); expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe('Term 1
\n`); + }); + + test('Multiline Definition List must have at least one non-newline character after ::', function() { + const source = 'Term 1\n::\nDefinition 1\n\n'; + const rendered = Markdown.render(source).trim(); + expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`Term 1
\n\nDefinition 1
`); + }); }); diff --git a/tests/markdown/hard-breaks.test.js b/tests/markdown/hard-breaks.test.js new file mode 100644 index 000000000..3d0f59a41 --- /dev/null +++ b/tests/markdown/hard-breaks.test.js @@ -0,0 +1,47 @@ +/* eslint-disable max-lines */ + +const Markdown = require('naturalcrit/markdown.js'); + +describe('Hard Breaks', ()=>{ + test('Single Break', function() { + const source = ':\n\n'; + const rendered = Markdown.render(source).trim(); + expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(``); + }); + + test('Double Break', function() { + const source = '::\n\n'; + const rendered = Markdown.render(source).trim(); + expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(``); + }); + + test('Triple Break', function() { + const source = ':::\n\n'; + const rendered = Markdown.render(source).trim(); + expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(``); + }); + + test('Many Break', function() { + const source = '::::::::::\n\n'; + const rendered = Markdown.render(source).trim(); + expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(``); + }); + + test('Multiple sets of Breaks', function() { + const source = ':::\n:::\n:::'; + const rendered = Markdown.render(source).trim(); + expect(rendered, `Input:\n${source}`, { showPrefix: false }).toBe(`\n\n`); + }); + + test('Break 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`);
+ });
+});